Skip to content

Commit 6407f42

Browse files
committed
feat: add exponential curve math and estimators
Signed-off-by: Brandon McAnsh <[email protected]>
1 parent 650c7f4 commit 6407f42

File tree

10 files changed

+865
-0
lines changed

10 files changed

+865
-0
lines changed

libs/currency-math/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
build/
2+
.gradle/
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
2+
3+
plugins {
4+
id(Plugins.android_library)
5+
id(Plugins.kotlin_android)
6+
id(Plugins.kotlin_ksp)
7+
id(Plugins.kotlin_serialization)
8+
id(Plugins.kotlin_parcelize)
9+
}
10+
11+
android {
12+
namespace = "${Gradle.codeNamespace}.libs.currency.math"
13+
compileSdk = Android.compileSdkVersion
14+
defaultConfig {
15+
minSdk = Android.minSdkVersion
16+
testInstrumentationRunner = Android.testInstrumentationRunner
17+
}
18+
19+
compileOptions {
20+
sourceCompatibility(Versions.java)
21+
targetCompatibility(Versions.java)
22+
}
23+
24+
java {
25+
toolchain {
26+
languageVersion.set(JavaLanguageVersion.of(Versions.java))
27+
}
28+
}
29+
30+
kotlinOptions {
31+
jvmTarget = JvmTarget.fromTarget(Versions.java).target
32+
freeCompilerArgs += listOf(
33+
"-opt-in=kotlin.ExperimentalUnsignedTypes",
34+
"-opt-in=kotlin.RequiresOptIn"
35+
)
36+
}
37+
}
38+
39+
dependencies {
40+
androidTestImplementation(Libs.androidx_junit)
41+
androidTestImplementation(Libs.junit)
42+
androidTestImplementation(Libs.androidx_test_runner)
43+
44+
implementation(Libs.inject)
45+
implementation(Libs.hilt)
46+
implementation(Libs.timber)
47+
implementation(Libs.bugsnag)
48+
49+
testImplementation(kotlin("test"))
50+
}
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
package com.flipcash.libs.currency.math
2+
3+
import com.flipcash.libs.currency.math.internal.DefaultMintMaxTokenSupply
4+
import com.flipcash.libs.currency.math.internal.FIXED_SCALE
5+
import com.flipcash.libs.currency.math.internal.exp
6+
import com.flipcash.libs.currency.math.internal.fromFixedBytes
7+
import com.flipcash.libs.currency.math.internal.ln
8+
import com.flipcash.libs.currency.math.internal.toFixedBytes
9+
import java.math.BigDecimal
10+
import java.math.MathContext
11+
import java.math.RoundingMode
12+
import java.text.DecimalFormat
13+
14+
@org.jetbrains.annotations.VisibleForTesting
15+
internal val mc: MathContext = MathContext(50, RoundingMode.HALF_EVEN)
16+
internal val formatter = DecimalFormat("0.000000000000000000") // 18 decimals
17+
18+
internal val START_PRICE = BigDecimal("0.01")
19+
internal val END_PRICE = BigDecimal("1000000")
20+
internal val MAX_SUPPLY = BigDecimal("$DefaultMintMaxTokenSupply")
21+
22+
// Constants for the default curve from $0.01 to $1_000_000 over 21_000_000 tokens
23+
private val CURVE_A = BigDecimal("11400.230149967394933471")
24+
private val CURVE_B = BigDecimal("0.000000877175273521")
25+
private val CURVE_C = CURVE_B
26+
27+
class ExponentialCurve(
28+
val a: BigDecimal,
29+
val b: BigDecimal,
30+
val c: BigDecimal
31+
) {
32+
companion object {
33+
fun getOrThrow(): ExponentialCurve {
34+
return ExponentialCurve(CURVE_A, CURVE_B, CURVE_C)
35+
}
36+
}
37+
38+
/**
39+
* Calculate token price at a given supply.
40+
* This function computes the spot price of a token based on the current supply
41+
* using the formula: R'(S) = a * b * e^(c * S)
42+
* where:
43+
* - R'(S) is the spot price at supply S
44+
* - S is the current supply of the token
45+
* - a, b, c are constants defining the curve's shape
46+
*
47+
* @param currentSupply The current supply of the token.
48+
* @return A [Result] containing the ccalculated spot price as a BigDecimal, or an exception if an arithmetic error occurs.
49+
*/
50+
fun spotPriceAtSupply(currentSupply: BigDecimal): Result<BigDecimal> {
51+
return runCatching {
52+
val cTimesS = c.multiply(currentSupply, mc)
53+
val exp = cTimesS.exp(mc)
54+
a.multiply(b, mc).multiply(exp, mc)
55+
}
56+
}
57+
58+
/**
59+
* Calculate the cost to buy a certain number of tokens given the current supply.
60+
* This function answers the question: "How much does it cost to buy X tokens?"
61+
* The formula used is: `(a * b / c) * (e^(c * new_supply) - e^(c * current_supply))`
62+
* where `new_supply = current_supply + tokensToBuy`.
63+
*
64+
* @param currentSupply The current supply of tokens.
65+
* @param tokensToBuy The number of tokens to be purchased.
66+
* @return A [Result] containing the calculated cost to buy the tokens, or an exception if an arithmetic error occurs.
67+
*/
68+
fun costToBuyTokens(
69+
currentSupply: BigDecimal,
70+
tokensToBuy: BigDecimal,
71+
): Result<BigDecimal> {
72+
return runCatching {
73+
val newSupply = currentSupply.add(tokensToBuy)
74+
val cs = c.multiply(currentSupply, mc)
75+
val ns = c.multiply(newSupply, mc)
76+
val expCs = cs.exp(mc)
77+
val expNs = ns.exp(mc)
78+
val abOverC = a.multiply(b, mc).divide(c, mc)
79+
val diff = expNs.subtract(expCs, mc)
80+
abOverC.multiply(diff, mc)
81+
}
82+
}
83+
84+
85+
/**
86+
* Calculate the value received when selling `tokensToSell` tokens.
87+
* This function answers the question: "How much value can I get for X tokens?"
88+
* The formula used is: `(a * b / c) * (e^(c * current_supply) - e^(c * new_supply))`
89+
* where `new_supply = current_supply - tokensToSell`.
90+
*
91+
* @param currentValue The current value before selling any tokens.
92+
* @param tokensToSell The number of tokens to be sold.
93+
* @return A [Result] containing the calculated value received from selling the tokens, or an exception if an arithmetic error occurs.
94+
*/
95+
fun valueFromSellingTokens(
96+
currentValue: BigDecimal,
97+
tokensToSell: BigDecimal,
98+
): Result<BigDecimal> {
99+
return runCatching {
100+
val abOverC = a.multiply(b, mc).divide(c, mc)
101+
val cvPlusAbOverC = currentValue.add(abOverC, mc)
102+
val cTimesTokensToSell = c.multiply(tokensToSell, mc)
103+
val negCTimesTokensToSell = cTimesTokensToSell.negate(mc)
104+
val exp = negCTimesTokensToSell.exp(mc)
105+
val oneMinsExp = BigDecimal.ONE.subtract(exp, mc)
106+
cvPlusAbOverC.multiply(oneMinsExp, mc)
107+
}
108+
}
109+
110+
/**
111+
* Calculate the number of tokens that can be bought for a given value, starting from the current supply.
112+
* This function answers the question: "How many tokens can I get for Y value?"
113+
* The formula used is: `num_tokens = (1/c) * ln(value / (a * b / c) + e^(c * current_supply)) - current_supply`
114+
*
115+
* @param currentSupply The current supply of tokens.
116+
* @param value The amount of value (e.g., currency) to be spent on tokens.
117+
* @return A [Result] containing the calculated number of tokens that can be bought, or an exception if an arithmetic error occurs.
118+
*/
119+
fun tokensBoughtForValue(
120+
currentSupply: BigDecimal,
121+
value: BigDecimal,
122+
): Result<BigDecimal> {
123+
return runCatching {
124+
val abOverC = a.multiply(b, mc).divide(c, mc)
125+
val expCs = currentSupply.multiply(c, mc).exp(mc)
126+
val term = value.divide(abOverC, mc).add(expCs, mc)
127+
val lnTerm = term.ln(mc)
128+
val result = lnTerm.divide(c, mc)
129+
result.subtract(currentSupply, mc)
130+
}
131+
}
132+
133+
/**
134+
* 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.
137+
* @param value The target value to receive from the exchange.
138+
* @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).
139+
*/
140+
fun tokensForValueExchange(
141+
currentSupply: BigDecimal,
142+
value: BigDecimal,
143+
): Result<BigDecimal> {
144+
return runCatching {
145+
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)
151+
}
152+
}
153+
154+
fun formattedTable(): String {
155+
return buildString {
156+
appendLine("|------|----------------|----------------------------------|----------------------------|")
157+
appendLine("| % | S | R(S) | R'(S) |")
158+
appendLine("|------|----------------|----------------------------------|----------------------------|")
159+
160+
val zero = BigDecimal.ZERO
161+
val buyAmount = BigDecimal("210000") // Adjusted for unscaled
162+
var supply = zero
163+
164+
for (i in 0..100) {
165+
val cost = costToBuyTokens(zero, supply).getOrThrow()
166+
val spotPrice = spotPriceAtSupply(supply).getOrThrow()
167+
appendLine(
168+
"| %3d%% | %14s | %32s | %26s |".format(
169+
i,
170+
supply.toBigInteger().toString(),
171+
formatter.format(cost),
172+
formatter.format(spotPrice)
173+
)
174+
)
175+
176+
supply = supply.add(buyAmount)
177+
}
178+
179+
appendLine("|------|----------------|----------------------------------|----------------------------|")
180+
}
181+
}
182+
}
183+
184+
data class RawExponentialCurve(
185+
val a: ByteArray,
186+
val b: ByteArray,
187+
val c: ByteArray
188+
) {
189+
companion object {
190+
fun fromStruct(parsed: ExponentialCurve): RawExponentialCurve {
191+
return RawExponentialCurve(
192+
parsed.a.toFixedBytes(),
193+
parsed.b.toFixedBytes(),
194+
parsed.c.toFixedBytes()
195+
)
196+
}
197+
}
198+
199+
fun toStruct(): ExponentialCurve? {
200+
return try {
201+
ExponentialCurve(
202+
fromFixedBytes(a, FIXED_SCALE),
203+
fromFixedBytes(b, FIXED_SCALE),
204+
fromFixedBytes(c, FIXED_SCALE)
205+
)
206+
} catch (e: Exception) {
207+
null // Or throw IOException if preferred
208+
}
209+
}
210+
211+
override fun equals(other: Any?): Boolean {
212+
if (this === other) return true
213+
if (javaClass != other?.javaClass) return false
214+
215+
other as RawExponentialCurve
216+
217+
if (!a.contentEquals(other.a)) return false
218+
if (!b.contentEquals(other.b)) return false
219+
if (!c.contentEquals(other.c)) return false
220+
221+
return true
222+
}
223+
224+
override fun hashCode(): Int {
225+
var result = a.contentHashCode()
226+
result = 31 * result + b.contentHashCode()
227+
result = 31 * result + c.contentHashCode()
228+
return result
229+
}
230+
}

0 commit comments

Comments
 (0)