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
9 changes: 8 additions & 1 deletion app/vibenet/demos/validity/ValidityDemo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -549,7 +549,14 @@ function ValidityDemoInner() {
try {
const price = priceOverrideWad ?? applyOffsetBps(spot, side, offsetBps);
const ammPrice = ammPriceFromQuote(price, vibeToken0);
const built = priceValidity(state.deployment.pair, k, ammPrice, ammSide(side, vibeToken0));
const ammSpot = ammPriceFromQuote(spot, vibeToken0);
const built = priceValidity(
state.deployment.pair,
k,
ammPrice,
ammSide(side, vibeToken0),
ammSpot,
);
return {
priceWad: price,
side,
Expand Down
38 changes: 38 additions & 0 deletions app/vibenet/demos/validity/lib/predicates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,44 @@ describe('predicates', () => {
expect(applyOffsetBps(WAD, 'sell', 0)).toBe(WAD);
});

it('stretches a satisfied buy box to the current point', () => {
const k = 1_000n * WAD * (1_000n * WAD); // mid 1.0
const current = WAD;
const target = 2n * WAD; // buy at ≤ 2 with mid at 1: already satisfied
const box = rectangleForTarget(k, target, 'buy', current);
const r0Now = sqrt((k * WAD) / current);
const r1Now = k / r0Now;
expect(box.r0Min <= r0Now && r0Now <= box.r0Max).toBe(true);
expect(box.r1Min <= r1Now && r1Now <= box.r1Max).toBe(true);
// Corners still respect price ≤ P.
expect((box.r1Max * WAD) / box.r0Min <= target).toBe(true);
});

it('stretches a satisfied sell box to the current point', () => {
const k = 1_000n * WAD * (1_000n * WAD); // mid 1.0
const current = WAD;
const target = WAD / 2n; // sell at ≥ 0.5 with mid at 1: already satisfied
const box = rectangleForTarget(k, target, 'sell', current);
const r0Now = sqrt((k * WAD) / current);
const r1Now = k / r0Now;
expect(box.r0Min <= r0Now && r0Now <= box.r0Max).toBe(true);
expect(box.r1Min <= r1Now && r1Now <= box.r1Max).toBe(true);
// Corners still respect price ≥ P.
expect((box.r1Min * WAD) / box.r0Max >= target).toBe(true);
});

it('keeps the resting box when the condition is not yet met', () => {
const k = 1_000n * WAD * (1_000n * WAD); // mid 1.0
const buyTarget = (99n * WAD) / 100n; // buy below mid rests as before
expect(rectangleForTarget(k, buyTarget, 'buy', WAD)).toEqual(
rectangleForTarget(k, buyTarget, 'buy'),
);
const sellTarget = (101n * WAD) / 100n; // sell above mid rests as before
expect(rectangleForTarget(k, sellTarget, 'sell', WAD)).toEqual(
rectangleForTarget(k, sellTarget, 'sell'),
);
});

it('buy box implies every corner has price ≤ P', () => {
const k = 1_000n * WAD * (1_000n * WAD);
const target = (99n * WAD) / 100n;
Expand Down
32 changes: 26 additions & 6 deletions app/vibenet/demos/validity/lib/predicates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,17 +93,33 @@ export function prettyValidity(predicates: ValidityPredicate[]): string {
* sell (price ≥ P): A/s ≤ r0 ≤ A ∧ B ≤ r1 ≤ B·s with B/A ≥ P
*
* Four storage predicates, so a drained or wildly expanded pool cannot fill.
*
* When `currentPriceWad` already satisfies the condition (a buy priced above
* the mid, a sell priced below it), the box stretches along the hyperbola to
* the current point so the order fills without the mid retracing to target.
*/
export function rectangleForTarget(k: bigint, targetPriceWad: bigint, side: Side): Rectangle {
export function rectangleForTarget(
k: bigint,
targetPriceWad: bigint,
side: Side,
currentPriceWad?: bigint,
): Rectangle {
if (k === 0n || targetPriceWad <= 0n) {
throw new Error('Need a live pool and a positive target price.');
}
const a = sqrt((k * WAD) / targetPriceWad);
if (a === 0n) throw new Error('Degenerate reserve bound.');
const aCur =
currentPriceWad !== undefined && currentPriceWad > 0n
? sqrt((k * WAD) / currentPriceWad)
: a;
if (side === 'buy') {
const b = (a * targetPriceWad) / WAD || 1n;
const r0Max = (a * BOX_SPAN_NUM) / BOX_SPAN_DEN;
const r1Min = (b * BOX_SPAN_DEN) / BOX_SPAN_NUM;
// Larger r0 means lower price; a satisfied buy sits at aOut > a.
const aOut = aCur > a ? aCur : a;
const bOut = k / aOut || 1n;
const r0Max = (aOut * BOX_SPAN_NUM) / BOX_SPAN_DEN;
const r1Min = (bOut * BOX_SPAN_DEN) / BOX_SPAN_NUM;
return {
r0Min: a,
r0Max: r0Max > a ? r0Max : a + 1n,
Expand All @@ -113,8 +129,11 @@ export function rectangleForTarget(k: bigint, targetPriceWad: bigint, side: Side
};
}
const b = (a * targetPriceWad + WAD - 1n) / WAD;
const r0Min = (a * BOX_SPAN_DEN) / BOX_SPAN_NUM;
const r1Max = (b * BOX_SPAN_NUM) / BOX_SPAN_DEN;
// Smaller r0 means higher price; a satisfied sell sits at aOut < a.
const aOut = aCur < a && aCur > 0n ? aCur : a;
const bOut = (k + aOut - 1n) / aOut;
const r0Min = (aOut * BOX_SPAN_DEN) / BOX_SPAN_NUM;
const r1Max = (bOut * BOX_SPAN_NUM) / BOX_SPAN_DEN;
return {
r0Min: r0Min < a ? r0Min : 1n,
r0Max: a,
Expand Down Expand Up @@ -151,8 +170,9 @@ export function priceValidity(
k: bigint,
targetPriceWad: bigint,
side: Side,
currentPriceWad?: bigint,
): { rectangle: Rectangle; predicates: ValidityPredicate[] } {
const rectangle = rectangleForTarget(k, targetPriceWad, side);
const rectangle = rectangleForTarget(k, targetPriceWad, side, currentPriceWad);
const r1MinValue = rectangle.r1Min << RESERVE_BITS;
const r1MaxValue = rectangle.r1Max << RESERVE_BITS;
const predicates: ValidityPredicate[] = [
Expand Down
Loading