Skip to content

Commit 87cd497

Browse files
committed
chore(currency-math): update tokensForValueExchange to use value locked as the primary input
also add additional logging to LocalFiat#valueExchangeIn Signed-off-by: Brandon McAnsh <[email protected]>
1 parent 634f47b commit 87cd497

File tree

7 files changed

+299
-71
lines changed

7 files changed

+299
-71
lines changed

libs/currency-math/src/main/kotlin/com/flipcash/libs/currency/math/Curves.kt

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -132,22 +132,26 @@ class ExponentialCurve(
132132

133133
/**
134134
* Calculate the number of tokens that can be exchanged for a given value, starting from the current supply.
135-
* This is the inverse of `valueFromSellingTokens`. It answers the question: "How many tokens should be exchanged for a value given the currentSupply?"
136-
* @param currentSupply The current supply of tokens.
135+
* This is the inverse of `valueFromSellingTokens`. It answers the question: "How many tokens should be exchanged for a value given the currentValue?"
136+
* @param currentValue The current supply of tokens.
137137
* @param value The target value to receive from the exchange.
138138
* @return A [Result] containing the calculated number of tokens for the exchange, or an exception if an arithmetic error occurs (e.g., input to log is not positive).
139139
*/
140140
fun tokensForValueExchange(
141-
currentSupply: BigDecimal,
141+
currentValue: BigDecimal,
142142
value: BigDecimal,
143143
): Result<BigDecimal> {
144144
return runCatching {
145145
val abOverC = a.multiply(b, mc).divide(c, mc)
146-
val expCs = currentSupply.multiply(c, mc).exp(mc)
147-
val abOverCTimesExpCs = abOverC.multiply(expCs, mc)
148-
val oneMinusFrac = BigDecimal.ONE.subtract(value.divide(abOverCTimesExpCs, mc), mc)
149-
val ln = oneMinusFrac.ln(mc)
150-
ln.negate(mc).divide(c, mc)
146+
val newValue = currentValue - value
147+
val currentValueOverAbOverC = currentValue.divide(abOverC, mc)
148+
val newValueOverAbOverC = newValue.divide(abOverC, mc)
149+
150+
val lnTerm1 = (BigDecimal.ONE.add(currentValueOverAbOverC)).ln(mc)
151+
val lnTerm2 = (BigDecimal.ONE.add(newValueOverAbOverC)).ln(mc)
152+
val diffLnTerms = lnTerm1.subtract(lnTerm2, mc)
153+
154+
diffLnTerms.divide(c, mc)
151155
}
152156
}
153157

libs/currency-math/src/main/kotlin/com/flipcash/libs/currency/math/Estimator.kt

Lines changed: 33 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package com.flipcash.libs.currency.math
33
import com.flipcash.libs.currency.math.internal.DefaultMintDecimals
44
import java.math.BigDecimal
55
import java.math.RoundingMode
6-
import kotlin.math.roundToLong
76

87
object Estimator {
98
/**
@@ -52,19 +51,20 @@ object Estimator {
5251
* 4. Scaling the result back to its "quark" representation (smallest indivisible unit).
5352
*
5453
* @param valueInQuarks The amount of value being exchanged, expressed in the value token's smallest unit (e.g., lamports for SOL, or the smallest unit for USDC).
55-
* @param currentSupplyInQuarks The current total supply of the token, expressed in its smallest unit ("quarks").
54+
* @param currentValueInQuarks The current total value locked in the bonding curve for this token,
55+
* expressed in the value token's smallest unit ("quarks").
5656
* @param mintDecimals The number of decimal places for the value token (e.g., SOL has 9, USDC typically has 6).
5757
* @return A [Result] containing the estimated number of quarks to be received as a [BigDecimal] on success.
5858
* On failure, it returns a `Result.failure` wrapping the exception.
5959
*/
6060
fun valueExchangeAsQuarks(
6161
valueInQuarks: Long,
62-
currentSupplyInQuarks: Long,
62+
currentValueInQuarks: Long,
6363
mintDecimals: Int,
6464
): Result<BigDecimal> {
6565
return runCatching {
6666
val tokenScale = BigDecimal.TEN.pow(DefaultMintDecimals, mc)
67-
val tokens = valueExchangeAsTokens(valueInQuarks, currentSupplyInQuarks, mintDecimals).getOrThrow()
67+
val tokens = valueExchangeAsTokens(valueInQuarks, currentValueInQuarks, mintDecimals).getOrThrow()
6868
val unscaledTokens = tokens.multiply(tokenScale, mc)
6969
unscaledTokens
7070
}
@@ -85,14 +85,15 @@ object Estimator {
8585
* the quantity of tokens that correspond to the input value.
8686
*
8787
* @param valueInQuarks The amount of value being exchanged, expressed in the value token's smallest unit (e.g., lamports for SOL, or the smallest unit for USDC).
88-
* @param currentSupplyInQuarks The current total supply of the token, expressed in its smallest unit ("quarks").
88+
* @param currentValueInQuarks The current total value locked in the bonding curve for this token,
89+
* expressed in the value token's smallest unit ("quarks").
8990
* @param mintDecimals The number of decimal places for the value token (e.g., SOL has 9, USDC typically has 6).
9091
* @return A [Result] containing the estimated number of tokens to be received as a [BigDecimal] on success.
9192
* On failure, it returns a `Result.failure` wrapping the exception.
9293
*/
9394
fun valueExchangeAsTokens(
9495
valueInQuarks: Long,
95-
currentSupplyInQuarks: Long,
96+
currentValueInQuarks: Long,
9697
mintDecimals: Int,
9798
): Result<BigDecimal> {
9899
return runCatching {
@@ -101,11 +102,11 @@ object Estimator {
101102
val unscaledValue = BigDecimal(valueInQuarks)
102103
val scaledValue = unscaledValue.divide(valueScale, mc)
103104

104-
val tokenScale = BigDecimal.TEN.pow(DefaultMintDecimals, mc)
105-
val unscaledCurrentSupply = BigDecimal(currentSupplyInQuarks)
106-
val scaledCurrentSupply = unscaledCurrentSupply.divide(tokenScale, mc)
105+
val tokenScale = BigDecimal.TEN.pow(mintDecimals, mc)
106+
val unscaledCurrentValue = BigDecimal(currentValueInQuarks)
107+
val scaledCurrentValue = unscaledCurrentValue.divide(tokenScale, mc)
107108

108-
curve.tokensForValueExchange(scaledCurrentSupply, scaledValue).getOrThrow()
109+
curve.tokensForValueExchange(scaledCurrentValue, scaledValue).getOrThrow()
109110
}
110111
}
111112

@@ -141,23 +142,19 @@ object Estimator {
141142
val scaledCurrentSupply = unscaledCurrentSupply.divideWithHighPrecision(tokenScale)
142143

143144
val scaledTokens = curve.tokensBoughtForValue(scaledCurrentSupply, scaledBuyAmount).getOrThrow()
144-
val unscaledTokens = scaledTokens.multiply(tokenScale, mc)
145+
val unscaledTokens = scaledTokens.multiplyWithHighPrecision(tokenScale)
145146

146147
val feePctValue = BigDecimal(feeBps).divideWithHighPrecision(BigDecimal("10000"))
147-
val scaledFees = scaledTokens.multiply(feePctValue, mc)
148-
val unscaledFees = scaledFees.multiply(tokenScale, mc)
149-
150-
val netTokens = unscaledTokens.subtract(unscaledFees, mc)
151-
.setScale(0, RoundingMode.HALF_EVEN)
152-
.toDouble().roundToLong()
153-
.toBigDecimal()
148+
val scaledFees = scaledTokens.multiplyWithHighPrecision(feePctValue)
149+
val unscaledFeesBD = scaledFees.multiplyWithHighPrecision(tokenScale).setScale(0, RoundingMode.DOWN)
150+
val unscaledFeesQuarks = unscaledFeesBD.longValueExact()
154151

155-
val tokensQuarks = netTokens
156-
val feesQuarks = unscaledFees
152+
val netTokensBD = unscaledTokens.subtract(unscaledFeesBD, mc).setScale(0, RoundingMode.DOWN)
153+
val netTokensQuarks = netTokensBD.longValueExact()
157154

158155
BuyEstimation(
159-
netTokensToReceive = tokensQuarks,
160-
fees = feesQuarks,
156+
netTokensToReceive = netTokensQuarks.toBigDecimal(),
157+
fees = unscaledFeesQuarks.toBigDecimal(),
161158
)
162159
}
163160
}
@@ -194,28 +191,28 @@ object Estimator {
194191
return runCatching {
195192
val curve = ExponentialCurve.getOrThrow()
196193

197-
val scale = BigDecimal.TEN.pow(DefaultMintDecimals, mc)
194+
val tokenScale = BigDecimal.TEN.pow(DefaultMintDecimals, mc)
198195
val unscaledSellAmount = BigDecimal(amountInQuarks, mc)
199-
val scaledSellAmount = unscaledSellAmount.divide(scale, mc)
196+
val scaledSellAmount = unscaledSellAmount.divideWithHighPrecision(tokenScale)
200197

201-
val tokenScale = BigDecimal.TEN.pow(mintDecimals, mc)
198+
val valueScale = BigDecimal.TEN.pow(mintDecimals, mc)
202199
val unscaledCurrentValue = BigDecimal(currentValueInQuarks, mc)
203-
val scaledCurrentValue = unscaledCurrentValue.divide(tokenScale, mc)
200+
val scaledCurrentValue = unscaledCurrentValue.divideWithHighPrecision(valueScale)
204201

205-
val scaledTokens = curve.valueFromSellingTokens(scaledCurrentValue, scaledSellAmount).getOrThrow()
206-
val unscaledTokens = scaledTokens.multiply(tokenScale, mc)
202+
val scaledValue = curve.valueFromSellingTokens(scaledCurrentValue, scaledSellAmount).getOrThrow()
203+
val unscaledValueBD = scaledValue.multiplyWithHighPrecision(valueScale)
207204

208-
val feePctValue = BigDecimal(feeBps).divide(BigDecimal("10000"), mc)
209-
val scaledFees = scaledTokens.multiply(feePctValue, mc)
210-
val unscaledFees = scaledFees.multiply(tokenScale, mc)
205+
val feePctValue = BigDecimal(feeBps).divideWithHighPrecision(BigDecimal("10000"))
206+
val scaledFees = scaledValue.multiplyWithHighPrecision(feePctValue)
207+
val unscaledFeesBD = scaledFees.multiplyWithHighPrecision(valueScale).setScale(0, RoundingMode.DOWN)
208+
val unscaledFeesQuarks = unscaledFeesBD.longValueExact()
211209

212-
val netAmount = unscaledTokens.subtract(unscaledFees, mc)
213-
val amountQuarks = netAmount
214-
val feesQuarks = unscaledFees
210+
val netAmountBD = unscaledValueBD.subtract(unscaledFeesBD, mc).setScale(0, RoundingMode.DOWN)
211+
val netAmountQuarks = netAmountBD.longValueExact()
215212

216213
SellEstimation(
217-
netAmountToReceive = amountQuarks,
218-
fees = feesQuarks,
214+
netAmountToReceive = netAmountQuarks.toBigDecimal(),
215+
fees = unscaledFeesQuarks.toBigDecimal(),
219216
)
220217
}
221218
}

libs/currency-math/src/main/kotlin/com/flipcash/libs/currency/math/Units.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ import com.flipcash.libs.currency.math.internal.DefaultMintQuarksPerUnit
44
import java.math.BigDecimal
55

66
fun BigDecimal.units(): BigDecimal = this.divide(BigDecimal(DefaultMintQuarksPerUnit), mc)
7-
fun BigDecimal.divideWithHighPrecision(other: BigDecimal): BigDecimal = this.divide(other, mc)
7+
fun BigDecimal.divideWithHighPrecision(other: BigDecimal): BigDecimal = this.divide(other, mc)
8+
fun BigDecimal.multiplyWithHighPrecision(other: BigDecimal): BigDecimal = this.multiply(other, mc)

libs/currency-math/src/test/java/com/flipcash/libs/currency/math/EstimationTests.kt

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,12 @@ class EstimationTests {
2929
@Test
3030
fun `estimate value exchange`() {
3131
val quarks = Estimator.valueExchangeAsQuarks(
32-
valueInQuarks = 5_000_000, // $5,
33-
currentSupplyInQuarks = 7232649000000000, // 723,264.9 tokens
32+
valueInQuarks = 10_000_000, // $10,
33+
currentValueInQuarks = 1_000_000_000, // $1000
3434
mintDecimals = 6,
3535
).getOrThrow()
3636

37-
val expectedAmount = BigInteger("2651496281136")
37+
val expectedAmount = BigInteger("9197272362330")
3838

3939
assertEquals(expectedAmount, quarks.toBigInteger())
4040
}
@@ -105,10 +105,10 @@ class EstimationTests {
105105
val startValue = 10_000L
106106
val endValue = 1_000_000_000_000_000L
107107

108-
println("value locked,payment value,payment quarks,sell value")
108+
println("value locked,total circulating supply,new circulating supply,payment value,payment quarks,sell value")
109109
var valueLocked = startValue
110110
while (valueLocked <= endValue) {
111-
val (circulatingSupply, _) = Estimator.buy(
111+
val (totalCirculatingSupply, _) = Estimator.buy(
112112
amountInQuarks = valueLocked,
113113
currentSupplyInQuarks = 0L,
114114
mintDecimals = 6,
@@ -119,7 +119,7 @@ class EstimationTests {
119119
while (paymentValue <= valueLocked) {
120120
val paymentQuarks = Estimator.valueExchangeAsQuarks(
121121
valueInQuarks = paymentValue,
122-
currentSupplyInQuarks = circulatingSupply.toLong(),
122+
currentValueInQuarks = valueLocked,
123123
mintDecimals = 6,
124124
).getOrThrow()
125125

@@ -130,9 +130,23 @@ class EstimationTests {
130130
feeBps = 0
131131
).getOrThrow()
132132

133+
val diff1 = paymentValue - sellValue.toLong()
134+
assertTrue("$paymentValue, $sellValue") { diff1 >= -1 && diff1 <= 1 }
135+
136+
val (newCirculatingSupply, _) = Estimator.buy(
137+
amountInQuarks = valueLocked - paymentValue,
138+
currentSupplyInQuarks = 0,
139+
mintDecimals = 6,
140+
feeBps = 0,
141+
).getOrThrow()
142+
143+
val totalLong = totalCirculatingSupply.toLong()
144+
val newLong = newCirculatingSupply.toLong()
145+
val paymentQLong = paymentQuarks.toLong()
146+
val diff2 = totalLong - newLong - paymentQLong
147+
assertTrue("$totalCirculatingSupply, $newCirculatingSupply, $paymentQuarks") { diff2 >= -1 && diff2 <= 1 }
133148

134-
assertTrue { abs(paymentValue.toDouble() - sellValue.toDouble()) <= 1.0 }
135-
println("$valueLocked,$paymentValue,${paymentQuarks},${sellValue}")
149+
println("$valueLocked,${totalCirculatingSupply.toLong()},${newCirculatingSupply.toLong()},$paymentValue,${paymentQuarks.toLong()},${sellValue.toLong()}")
136150
paymentValue *= 10L
137151
}
138152
valueLocked *= 10L

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ data class Fiat(
9090
}
9191
minimumFractionDigits = preferredDigits
9292
maximumFractionDigits = preferredDigits
93-
roundingMode = if (decimalDigits == 0) RoundingMode.DOWN else ROUNDING_MODE
93+
roundingMode = ROUNDING_MODE
9494
(this as DecimalFormat).decimalFormatSymbols =
9595
decimalFormatSymbols.apply {
9696
currencySymbol = ""
@@ -147,7 +147,7 @@ data class Fiat(
147147

148148
val tokens = (Estimator.valueExchangeAsTokens(
149149
valueInQuarks = this.quarks,
150-
currentSupplyInQuarks = token?.launchpadMetadata?.currentCirculatingSupplyQuarks ?: 0,
150+
currentValueInQuarks = token?.launchpadMetadata?.coreMintLockedQuarks ?: 0,
151151
mintDecimals = 6,
152152
).getOrNull() ?: BigDecimal.ZERO)
153153

@@ -181,11 +181,10 @@ data class Fiat(
181181

182182
fun tokenBalance(
183183
quarks: Long,
184-
currencyCode: CurrencyCode = CurrencyCode.USD,
185184
token: Token
186185
): Fiat {
187186
if (token.address == Mint.usdc) {
188-
return Fiat(quarks, currencyCode)
187+
return Fiat(quarks, CurrencyCode.USD)
189188
}
190189

191190
return Fiat(

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

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.getcode.opencode.model.financial
33
import com.flipcash.libs.currency.math.Estimator
44
import com.flipcash.libs.currency.math.divideWithHighPrecision
55
import com.flipcash.libs.currency.math.units
6+
import com.getcode.opencode.model.financial.Fiat.Formatting
67
import com.getcode.opencode.model.transactions.ExchangeData
78
import com.getcode.services.opencode.BuildConfig
89
import com.getcode.solana.keys.Mint
@@ -83,14 +84,14 @@ data class LocalFiat(
8384
}
8485
}
8586

86-
val circulatingSupply = token.launchpadMetadata?.currentCirculatingSupplyQuarks ?: 0
87+
val valueLocked = token.launchpadMetadata?.coreMintLockedQuarks ?: 0
8788

8889
val cappedValue = balance?.let { min(it, usdValue) } ?: usdValue
8990

9091
// determine quarks to exchange for the desired amount
9192
val quarks = Estimator.valueExchangeAsQuarks(
9293
valueInQuarks = cappedValue.quarks,
93-
currentSupplyInQuarks = circulatingSupply,
94+
currentValueInQuarks = valueLocked,
9495
mintDecimals = 6, // usdc is 6 decimals
9596
).getOrThrow()
9697

@@ -103,20 +104,60 @@ data class LocalFiat(
103104
// USD is a 1:1 fx so we can be blind here
104105
val fx = rate.fx * usdFx.toDouble()
105106

106-
val sellAmountUsd = Fiat.tokenBalance(quarks.toLong(), token = token)
107+
val underlyingTokenAmount = Fiat(quarks.toLong(), CurrencyCode.USD)
108+
val sellEstimate = Fiat.tokenBalance(quarks.toLong(), token).convertingTo(rate)
109+
110+
logExchange(
111+
debug = debug,
112+
rate = rate,
113+
amount = amount,
114+
usdValue = usdValue,
115+
balance = balance,
116+
cappedValue = cappedValue,
117+
token = token,
118+
valueLocked = valueLocked,
119+
quarks = quarks,
120+
units = units,
121+
fx = fx,
122+
sellEstimate = sellEstimate
123+
)
124+
125+
return LocalFiat(
126+
underlyingTokenAmount = underlyingTokenAmount,
127+
// our native amount for the transfer is the valuation of the quarks from a sell
128+
nativeAmount = amount,
129+
mint = token.address,
130+
rate = Rate(fx = fx, currency = rate.currency),
131+
)
132+
}
107133

134+
private fun logExchange(
135+
debug: Boolean,
136+
rate: Rate,
137+
amount: Fiat,
138+
usdValue: Fiat,
139+
balance: Fiat?,
140+
cappedValue: Fiat,
141+
token: Token,
142+
valueLocked: Long,
143+
quarks: BigDecimal,
144+
units: BigDecimal,
145+
fx: Double,
146+
sellEstimate: Fiat,
147+
) {
108148
if (debug) {
109149
println("############## EXCHANGE REPORT ###################")
110150
println("requested currency: ${rate.currency.name}")
151+
println("original currency fx: ${rate.fx}")
111152
println("requested amount: ${amount.formatted()}")
112153
println("requested quarks (in USD): ${usdValue.quarks * 1_000_000}")
113154
println("balance quarks (in USD): ${balance?.quarks?.times(1_000_000)}")
114155
println("capped quarks (in USD): ${cappedValue.quarks * 1_000_000}")
115-
println("circulating supply (of ${token.symbol}): $circulatingSupply")
156+
println("locked value (of ${token.symbol}): $valueLocked")
116157
println("calculated quarks: $quarks")
117158
println("units: $units")
118159
println("fx: $fx")
119-
println("sellAmount: ${sellAmountUsd.formatted(formatting = Fiat.Formatting.Length(token.decimals))}")
160+
println("sell estimate: ${sellEstimate.formatted(formatting = Formatting.Length(token.decimals))}")
120161
println("##################################################")
121162
}
122163

@@ -125,24 +166,18 @@ data class LocalFiat(
125166
message = "Bill created",
126167
metadata = {
127168
"requested currency" to rate.currency.name
169+
"original currency fx" to rate.fx
128170
"requested amount" to amount.formatted()
129171
"requested quarks (in USD)" to usdValue.quarks * 1_000_000
130172
"balance quarks (in USD)" to balance?.quarks?.times(1_000_000)
131173
"capped quarks (in USD)" to cappedValue.quarks * 1_000_000
132-
"circulating supply of ${token.symbol}" to circulatingSupply
174+
"locked value of ${token.symbol}" to valueLocked
133175
"calculated quarks" to quarks
134176
"units" to units
135177
"fx" to fx
136-
"sell amount (in USD)" to sellAmountUsd.formatted(formatting = Fiat.Formatting.Length(token.decimals))
178+
"sell estimate" to sellEstimate.formatted(formatting = Formatting.Length(token.decimals))
137179
}
138180
)
139-
140-
return LocalFiat(
141-
underlyingTokenAmount = Fiat(quarks.toLong(), CurrencyCode.USD),
142-
nativeAmount = amount,
143-
mint = token.address,
144-
rate = Rate(fx = fx, currency = rate.currency)
145-
)
146181
}
147182
}
148183
}

0 commit comments

Comments
 (0)