Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions src/vrgda/VRGDADecayMath.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,36 +11,43 @@ library VRGDADecayMath {
}

/// @notice Returns the cost of a purchase over a continuous VRGDA with exponential decay
/// @dev Integrates the VRGDA price curve over token amount. Since from/to are in auction-time,
/// the change of variables from token-space introduces a Jacobian factor of emission (dn = emission * dt).
/// @param timestamp The timestamp of the start of the VRGDA
/// @param price The price coefficient of the VRGDA
/// @param price The target price per token at neutral schedule
/// @param decay The decay coefficient of the VRGDA
/// @param emission The issuance rate (tokens per day)
/// @param from Where the VRGDA has sold up to in its issuance schedule as of current block.timestamp (days)
/// @param to Where the VRGDA will have sold up to in its issuance schedule after the current purchase (days)
/// @return cost The cost of the purchase
function exponentialDecay(UFixed18 timestamp, UFixed18 price, UFixed18 decay, UFixed18 from, UFixed18 to) internal view returns (UFixed18 cost) {
function exponentialDecay(UFixed18 timestamp, UFixed18 price, UFixed18 decay, UFixed18 emission, UFixed18 from, UFixed18 to) internal view returns (UFixed18 cost) {
(Fixed18 a, Fixed18 b) = (convert(timestamp + to), convert(timestamp + from));

Fixed18 sDecay = Fixed18Lib.from(decay);

return price * UFixed18Lib.from((-sDecay * a).exp() - (-sDecay * b).exp()) / decay;
return price * emission * UFixed18Lib.from((-sDecay * a).exp() - (-sDecay * b).exp()) / decay;
}

/// @notice Returns time of the latest auction after the purchase over a continuous VRGDA with exponential decay
/// @dev Solves the corrected forward formula: cost = price * emission * (exp(-decay*a) - exp(-decay*b)) / decay
/// @param timestamp The timestamp of the start of the VRGDA
/// @param price The price coefficient of the VRGDA
/// @param price The target price per token at neutral schedule
/// @param decay The decay coefficient of the VRGDA
/// @param emission The issuance rate (tokens per day)
/// @param from The time of the latest auction relative to the start of the VRGDA
/// @param cost The cost of the purchase
/// @return to The time of the latest auction after the purchase relative to the start of the VRGDA
function exponentialDecayI(UFixed18 timestamp, UFixed18 price, UFixed18 decay, UFixed18 from, UFixed18 cost) internal view returns (UFixed18 to) {
function exponentialDecayI(UFixed18 timestamp, UFixed18 price, UFixed18 decay, UFixed18 emission, UFixed18 from, UFixed18 cost) internal view returns (UFixed18 to) {
Fixed18 b = convert(timestamp + from);

(Fixed18 sDecay, Fixed18 sPrice, Fixed18 sCost) = (Fixed18Lib.from(decay), Fixed18Lib.from(price), Fixed18Lib.from(cost));
(Fixed18 sDecay, Fixed18 sPrice, Fixed18 sCost, Fixed18 sEmission) = (Fixed18Lib.from(decay), Fixed18Lib.from(price), Fixed18Lib.from(cost), Fixed18Lib.from(emission));

// increase precision by inverting the input to exp() if b is negative
Fixed18 exp = b >= Fixed18Lib.ZERO ? sPrice / (sDecay * b).exp() : sPrice * (-sDecay * b).exp();

Fixed18 a = ln(sPrice, (sCost * sDecay + exp)) / sDecay;
// scale both sides of the ln ratio by emission to avoid precision loss from / sEmission
// ln(price, cost*decay/emission + exp) = ln(price*emission, cost*decay + exp*emission)
Fixed18 a = ln(sPrice * sEmission, (sCost * sDecay + exp * sEmission)) / sDecay;

return convert(a) - timestamp;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
Expand Down
2 changes: 2 additions & 0 deletions src/vrgda/types/LinearExponentialVRGDA.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ library LinearExponentialVRGDALib {
self.timestamp,
self.price,
self.decay,
self.emission,
VRGDAIssuanceMath.linearIssuanceI(self.emission, issued),
VRGDAIssuanceMath.linearIssuanceI(self.emission, issued + amount)
);
Expand All @@ -42,6 +43,7 @@ library LinearExponentialVRGDALib {
self.timestamp,
self.price,
self.decay,
self.emission,
VRGDAIssuanceMath.linearIssuanceI(self.emission, issued),
cost
)
Expand Down
61 changes: 40 additions & 21 deletions test/vrgda/LinearExponentialVRGDA.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,75 +18,94 @@ contract LinearExponentialVRGDATest is RootTest {
});
}

/// @notice At neutral time (exactly on schedule), the marginal cost of a small purchase
/// should approximate price * amount, confirming the emission Jacobian is correct.
function test_neutralTimePriceEqualsTarget() public {
// set up a clean neutral-time scenario:
// warp so that time() - timestamp = 1 day exactly, with issued = emission * 1 day = 200 tokens
vm.warp(86400 + 86400); // time() = 2.0 days, timestamp = 1.0 day, elapsed = 1.0 day
issued = UFixed18Lib.from(200); // exactly on schedule

// buy a small amount (0.001 tokens) — marginal cost should be ≈ price * amount = 100 * 0.001 = 0.1
UFixed18 smallAmount = UFixed18.wrap(0.001e18);
UFixed18 cost = vrgda.toCost(issued, smallAmount);
// expected ≈ 0.1e18, allow 10% tolerance for the small-but-finite purchase size
assertApproxEqRel(UFixed18.unwrap(cost), 0.1e18, 0.1e18, "neutral time price should approximate price * amount");

// confirm the inverse: spending that cost should yield ≈ smallAmount tokens
UFixed18 recovered = vrgda.toAmount(issued, cost);
assertApproxEqRel(UFixed18.unwrap(recovered), UFixed18.unwrap(smallAmount), 0.1e18, "neutral time toAmount(toCost(x)) should approximate x");
}

function test_costDecreasesWhenBehind() public {
// initial cost to purchase 1 token quite high
assertUFixed18Eq(vrgda.toCost(issued, UFixed18Lib.from(1)), UFixed18.wrap(11_291.903496976_577338030e18));
assertUFixed18Eq(vrgda.toCost(issued, UFixed18Lib.from(1)), UFixed18.wrap(2_258_380.699395315_467606000e18));
// cost to move ahead of issuance schedule is higher
assertUFixed18Eq(vrgda.toCost(issued, UFixed18Lib.from(100)), UFixed18.wrap(32_466_151.192919613_034886620e18));
assertUFixed18Eq(vrgda.toCost(issued, UFixed18Lib.from(100)), UFixed18.wrap(6_493_230_238.583922606_977324000e18));

// after 1 day with nothing sold, cost to purchase 100 tokens is reasonable, and purchase is made
skip(1 days);
assertUFixed18Eq(vrgda.toCost(issued, UFixed18Lib.from(100)), UFixed18.wrap(1_473.960983816_764210610e18));
assertUFixed18Eq(vrgda.toCost(issued, UFixed18Lib.from(100)), UFixed18.wrap(294_792.196763352_842122000e18));
issued = UFixed18Lib.from(100);
// price increases as a result, but we're still behind
assertUFixed18Eq(vrgda.toCost(issued, UFixed18Lib.from(100)), UFixed18.wrap(218_755.206002187_764655650e18));
assertUFixed18Eq(vrgda.toCost(issued, UFixed18Lib.from(100)), UFixed18.wrap(43_751_041.200437552_931130000e18));

// after half a day, price becomes reasonable again, and continues to decrease
skip(12 hours);
assertUFixed18Eq(vrgda.toCost(issued, UFixed18Lib.from(100)), UFixed18.wrap(1_473.960983816_764210610e18));
assertUFixed18Eq(vrgda.toCost(issued, UFixed18Lib.from(100)), UFixed18.wrap(294_792.196763352_842122000e18));
skip(12 hours);
assertUFixed18Eq(vrgda.toCost(issued, UFixed18Lib.from(100)), UFixed18.wrap(9.931470987_677229170e18));
assertUFixed18Eq(vrgda.toCost(issued, UFixed18Lib.from(100)), UFixed18.wrap(1_986.294197535_445834000e18));
}

function test_amountIncreasesWhenBehind() public {
// after 1 day, 200 tokens have been purchased, even with issuance schedule
skip(1 days);
issued = UFixed18Lib.from(200);
// a 5k purchase would buy us 200 tokens
assertUFixed18Eq(vrgda.toAmount(issued, UFixed18Lib.from(5_000)), UFixed18.wrap(0.448974472_612174000e18));
// a 5k purchase buys tokens
assertUFixed18Eq(vrgda.toAmount(issued, UFixed18Lib.from(5_000)), UFixed18.wrap(0.002270130_392229200e18));

// 8 hours later we would be behind issuance schedule, and should be able to purchase more tokens
skip(8 hours);
assertUFixed18Eq(vrgda.toAmount(issued, UFixed18Lib.from(5_000)), UFixed18.wrap(9.849858676_957755400e18));
assertUFixed18Eq(vrgda.toAmount(issued, UFixed18Lib.from(5_000)), UFixed18.wrap(0.063538021_305059800e18));

// after 4 days, should be able to purchase considerably more
skip(3 days + 16 hours);
assertUFixed18Eq(vrgda.toAmount(issued, UFixed18Lib.from(5_000)), UFixed18.wrap(724.294476783_258653200e18));
assertUFixed18Eq(vrgda.toAmount(issued, UFixed18Lib.from(5_000)), UFixed18.wrap(618.328129452_298664400e18));
}

function test_amountDecreasesWhenAhead() public {
// after 1 day, 200 tokens have been purchased, even with issuance schedule
skip(1 days);
issued = UFixed18Lib.from(200);
// a 50k purchase would buy us 4 tokens
assertUFixed18Eq(vrgda.toAmount(issued, UFixed18Lib.from(50_000)), UFixed18.wrap(4.091865860_077915200e18));
// a 50k purchase buys tokens
assertUFixed18Eq(vrgda.toAmount(issued, UFixed18Lib.from(50_000)), UFixed18.wrap(0.022689716_894178400e18));

// if someone purchases 100 tokens, the same 50k purchase would buy us almost nothing
issued = issued + UFixed18Lib.from(100);
assertUFixed18Eq(vrgda.toAmount(issued, UFixed18Lib.from(50_000)), UFixed18.wrap(0.030570397_153121600e18));
assertUFixed18Eq(vrgda.toAmount(issued, UFixed18Lib.from(50_000)), UFixed18.wrap(0.000152968_278971800e18));

// if even more overbought, the amount we can purchase becomes infinitesimal
issued = issued + UFixed18Lib.from(200);
assertUFixed18Eq(vrgda.toAmount(issued, UFixed18Lib.from(50_000)), UFixed18.wrap(0.000001388_955087400e18));
assertUFixed18Eq(vrgda.toAmount(issued, UFixed18Lib.from(50_000)), UFixed18.wrap(0.000000006_944775400e18));
}

function test_costIncreasesWhenAhead() public {
// after 1 day, 200 tokens have been purchased, even with issuance schedule
skip(1 days);
issued = UFixed18Lib.from(200);
assertUFixed18Eq(vrgda.toCost(issued, UFixed18Lib.from(1)), UFixed18.wrap(11_291.903496976_577338030e18));
assertUFixed18Eq(vrgda.toCost(issued, UFixed18Lib.from(1)), UFixed18.wrap(2_258_380.699395315_467606000e18));
// price to purchase ahead of issuance schedule is higher
assertUFixed18Eq(vrgda.toCost(issued, UFixed18Lib.from(100)), UFixed18.wrap(32_466_151.192919613_034886620e18));
assertUFixed18Eq(vrgda.toCost(issued, UFixed18Lib.from(100)), UFixed18.wrap(6_493_230_238.583922606_977324000e18));

// after 3 days, expected issuance is 600 tokens, and we've sold 650, slightly ahead of schedule
issued = issued + UFixed18Lib.from(450);
skip(2 days);
assertUFixed18Eq(vrgda.toCost(issued, UFixed18Lib.from(100)), UFixed18.wrap(395_518_690.835029055_174152540e18));
assertUFixed18Eq(vrgda.toCost(issued, UFixed18Lib.from(100)), UFixed18.wrap(79_103_738_167.005811034_830508000e18));

// a day later we're now at 900, when 800 was expected; cost continues to increase
issued = issued + UFixed18Lib.from(250);
skip(1 days);
assertUFixed18Eq(vrgda.toCost(issued, UFixed18Lib.from(100)), UFixed18.wrap(4_818_404_062.443085713_321147380e18));
assertUFixed18Eq(vrgda.toCost(issued, UFixed18Lib.from(100)), UFixed18.wrap(963_680_812_488.617142664_229476000e18));
}

function test_toCostEquivalentWithToAmount() public {
Expand All @@ -107,17 +126,17 @@ contract LinearExponentialVRGDATest is RootTest {
// we're 3 days into the auction, and have only sold 50 tokens; cost to purchase 100 tokens is quite low
skip(3 days);
issued = UFixed18Lib.from(50);
assertUFixed18Eq(vrgda.toCost(issued, UFixed18Lib.from(100)), UFixed18.wrap(0.000037011_147859640e18));
assertUFixed18Eq(vrgda.toCost(issued, UFixed18Lib.from(100)), UFixed18.wrap(0.007402229_571928000e18));

// another day passes, and price for 100 tokens hits zero, but 500 tokens has some cost
skip(3 days);
assertUFixed18Eq(vrgda.toCost(issued, UFixed18Lib.from(100)), UFixed18.wrap(0));
assertUFixed18Eq(vrgda.toCost(issued, UFixed18Lib.from(500)), UFixed18.wrap(1_691702110));
assertUFixed18Eq(vrgda.toCost(issued, UFixed18Lib.from(500)), UFixed18.wrap(338340422000));
// user grabs 100 tokens for free
issued = issued + UFixed18Lib.from(100);

// we're 6 days in, and should have sold 1200 tokens; price recovers upon doing so
assertUFixed18Eq(vrgda.toCost(issued, UFixed18Lib.from(1050)), UFixed18.wrap(220_239.165828664_098470190e18));
assertUFixed18Eq(vrgda.toCost(issued, UFixed18Lib.from(1050)), UFixed18.wrap(44_047_833.165732819_694038000e18));
}

/// @dev Ensures that users may not use split purchases into smaller batches to reduce cost
Expand Down
Loading
Loading