Skip to content

Commit

Permalink
test: stable oracle math
Browse files Browse the repository at this point in the history
* test: add cases for spot price calculation
* fix: safeguards for not reverting
* test: use estimates
* lint: fix issues
* ci: update hash
* gas: update snapshot
* ci: update hash
* ci: update hashes
* gas: update snapshot
  • Loading branch information
xenide authored Dec 11, 2024
1 parent 3985f40 commit 39d094b
Show file tree
Hide file tree
Showing 8 changed files with 250 additions and 108 deletions.
199 changes: 102 additions & 97 deletions .gas-snapshot

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"gas:check": "forge snapshot --check",
"generate": "typechain --target ethers-v5 --out-dir typings 'out/**/*.json'",
"install": "npm run install:balancer",
"install:balancer": "cd reference/balancer-v2-monorepo && yarn",
"install:balancer": "cd reference/balancer-v2-monorepo && yarn && yarn workspace @balancer-labs/balancer-js build",
"lint": "npm run lint:check",
"lint:check": "npm run solhint:check && npm run prettier:check && npm run mdlint:check",
"lint:fix": "npm run solhint:fix && npm run prettier:fix && npm run eslint:fix && npm run mdlint:fix",
Expand Down
2 changes: 1 addition & 1 deletion script/optimized-deployer-meta
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"constant_product_hash": "0xe174de1f7ab5f7c871f23787d956a8d1b4ebbb3b195eb2d6af27fb3a8c9e812e",
"factory_hash": "0x87b0f73fafcf4bb41e013c8423dc679f6885527007d6c3f1e1834a670cbaadc5",
"stable_hash": "0x3ae886aee24fa2cc0144d24306033a7ed47e91bc0f962e4bffcef5922ae175f5"
"stable_hash": "0x6f62531ebc702a07ab48405ba9437786af3664b83b0da26ec295402590ed738f"
}
2 changes: 1 addition & 1 deletion script/unoptimized-deployer-meta
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"constant_product_hash": "0x89ba88e63d531f6343d8f30d0baf35d10eb900e4e9e5cf64e857a17d63c27863",
"factory_hash": "0x09a9ce1ed77c95be4842dddd771939e048b8bfe2837863be3a2766b1c13ea5a2",
"stable_hash": "0x2556755e769639b4fbeff548907a3a675f94209a24a97c7fca31eedb76567c2f"
"stable_hash": "0xe1770a7f5ab45f8da1a575db115e93cc8f48d5c768f72090dfe9cc85c62dc8d4"
}
2 changes: 1 addition & 1 deletion src/ReservoirDeployer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ contract ReservoirDeployer {
bytes32 public constant FACTORY_HASH = bytes32(0x87b0f73fafcf4bb41e013c8423dc679f6885527007d6c3f1e1834a670cbaadc5);
bytes32 public constant CONSTANT_PRODUCT_HASH =
bytes32(0xe174de1f7ab5f7c871f23787d956a8d1b4ebbb3b195eb2d6af27fb3a8c9e812e);
bytes32 public constant STABLE_HASH = bytes32(0x3ae886aee24fa2cc0144d24306033a7ed47e91bc0f962e4bffcef5922ae175f5);
bytes32 public constant STABLE_HASH = bytes32(0x6f62531ebc702a07ab48405ba9437786af3664b83b0da26ec295402590ed738f);

// Deployment addresses.
GenericFactory public factory;
Expand Down
19 changes: 12 additions & 7 deletions src/libraries/StableOracleMath.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
pragma solidity ^0.8.0;

import { FixedPointMathLib } from "solady/utils/FixedPointMathLib.sol";

import { LogCompression } from "src/libraries/LogCompression.sol";
import { StableMath } from "src/libraries/StableMath.sol";

Expand All @@ -26,7 +25,7 @@ library StableOracleMath {
logSpotPrice = LogCompression.toLowResLog(spotPrice);
}

/// @notice Calculates the spot price of token1 in token0.
/// @notice Calculates the spot price of token1 in token0. i.e. token0 is base, token1 is quote
/// @param amplificationParameter The stable amplification parameter in precise form (see StableMath.A_PRECISION).
/// @param reserve0 The reserve of token0 normalized to 18 decimals.
/// @param reserve1 The reserve of token1 normalized to 18 decimals.
Expand Down Expand Up @@ -55,15 +54,21 @@ library StableOracleMath {

uint256 axy2 = (a * 2 * reserve0).mulWad(reserve1); // n = 2

uint256 by = b.mulWad(reserve1);
uint256 ay2 = ((a * reserve1).mulWad(reserve1));
if (by > axy2 + ay2) return 1e18;
// dx = a.x.y.2 + a.y^2 - b.y
uint256 derivativeX = axy2 + ((a * reserve1).mulWad(reserve1)) - (b.mulWad(reserve1));
uint256 derivativeX = axy2 + ay2 - by;

// dy = a.x.y.2 + a.x^2 - b.x
uint256 derivativeY = axy2 + ((a * reserve0).mulWad(reserve0)) - (b.mulWad(reserve0));
uint256 bx = (b.mulWad(reserve0));
uint256 ax2 = ((a * reserve0).mulWad(reserve0));
if (bx > axy2 + ax2) return 1e18;
uint256 derivativeY = axy2 + ax2 - bx;

if (derivativeY == 0 || derivativeX == 0) {
return 1e18;
}
// This is to prevent division by 0 which happens if reserve0 and reserve1 are sufficiently small (~1e6 after normalization) which can brick the pair
// If the reserves are that small, their prices will not be serving as price oracles, thus this is safe.
if (derivativeY == 0) return 1e18;

// The rounding direction is irrelevant as we're about to introduce a much larger error when converting to log
// space. We use `divWadUp` as it prevents the result from being zero, which would make the logarithm revert. A
Expand Down
46 changes: 46 additions & 0 deletions test/__mocks/StableOracleMathCanonical.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.10;

import { FixedPointMathLib } from "solady/utils/FixedPointMathLib.sol";
import { StableMath } from "src/libraries/StableMath.sol";

// the original implementation without safeguards as implemented by balancer
// https://github.com/balancer/balancer-v2-monorepo/blob/903d34e491a5e9c5d59dabf512c7addf1ccf9bbd/pkg/pool-stable/contracts/meta/StableOracleMath.sol
library StableOracleMathCanonical {
using FixedPointMathLib for uint256;

function calcSpotPrice(uint256 amplificationParameter, uint256 reserve0, uint256 reserve1)
internal
view
returns (uint256 spotPrice)
{
// //
// 2.a.x.y + a.y^2 + b.y //
// spot price Y/X = - dx/dy = ----------------------- //
// 2.a.x.y + a.x^2 + b.x //
// //
// n = 2 //
// a = amp param * n //
// b = D + a.(S - D) //
// D = invariant //
// S = sum of balances but x,y = 0 since x and y are the only tokens //

uint256 invariant =
StableMath._computeLiquidityFromAdjustedBalances(reserve0, reserve1, 2 * amplificationParameter);

uint256 a = (amplificationParameter * 2) / StableMath.A_PRECISION;
uint256 b = (invariant * a) - invariant;
uint256 axy2 = (a * 2 * reserve0).mulWad(reserve1); // n = 2

// dx = a.x.y.2 + a.y^2 - b.y
uint256 derivativeX = axy2 + ((a * reserve1).mulWad(reserve1)) - (b.mulWad(reserve1));

// dy = a.x.y.2 + a.x^2 - b.x
uint256 derivativeY = axy2 + ((a * reserve0).mulWad(reserve0)) - (b.mulWad(reserve0));

// The rounding direction is irrelevant as we're about to introduce a much larger error when converting to log
// space. We use `divWadUp` as it prevents the result from being zero, which would make the logarithm revert. A
// result of zero is therefore only possible with zero balances, which are prevented via other means.
spotPrice = derivativeX.divWadUp(derivativeY);
}
}
86 changes: 86 additions & 0 deletions test/unit/libraries/StableOracleMath.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.10;

import "forge-std/Test.sol";
import { FixedPointMathLib } from "solady/utils/FixedPointMathLib.sol";
import { StableOracleMath, StableMath } from "src/libraries/StableOracleMath.sol";
import { Constants } from "src/Constants.sol";
import { StableOracleMathCanonical } from "test/__mocks/StableOracleMathCanonical.sol";

contract StableOracleMathTest is Test {
using FixedPointMathLib for uint256;

uint256 internal _defaultAmp = Constants.DEFAULT_AMP_COEFF * StableMath.A_PRECISION;

// estimates the spot price by giving a very small input to simulate dx (an infinitesimally small x)
function estimateSpotPrice(
uint256 reserve0,
uint256 reserve1,
uint256 token0Multiplier,
uint256 token1Multiplier,
uint256 N_A
) internal pure returns (uint256 rPrice) {
uint256 lInputAmt = 1e8; // anything smaller than 1e7 the error becomes larger, as experimented
uint256 lOut = StableMath._getAmountOut(lInputAmt, reserve0, reserve1, token0Multiplier, token1Multiplier, true, 0, N_A);
rPrice = lOut.divWadUp(lInputAmt);
}

function testPrice_Token0MoreExpensive() external {
// arrange
uint256 lToken0Amt = 1_000_000e18;
uint256 lToken1Amt = 2_000_000e18;

// act
uint256 lPrice = StableOracleMath.calcSpotPrice(_defaultAmp, lToken0Amt, lToken1Amt);

// assert
assertEq(lPrice, 1_000_842_880_946_746_931);
}

function testPrice_Token1MoreExpensive() external {
// arrange
uint256 lToken0Amt = 2_000_000e18;
uint256 lToken1Amt = 1_000_000e18;

// act
uint256 lPrice = StableOracleMath.calcSpotPrice(_defaultAmp, lToken0Amt, lToken1Amt);

// assert
assertEq(lPrice, 999_157_828_903_224_444);
}

function testCalcSpotPrice_CanonicalVersion_VerySmallAmounts(uint256 aToken0Amt, uint256 aToken1Amt) external {
// assume - if token amounts exceed these amounts then they will probably not revert
uint256 lToken0Amt = bound(aToken0Amt, 1, 1e6);
uint256 lToken1Amt = bound(aToken1Amt, 1, 6e6);

// act & assert - reverts when the amount is very small
vm.expectRevert();
StableOracleMathCanonical.calcSpotPrice(_defaultAmp, lToken0Amt, lToken1Amt);
}

function testCalcSpotPrice_VerySmallAmounts(uint256 aToken0Amt, uint256 aToken1Amt) external {
// assume
uint256 lToken0Amt = bound(aToken0Amt, 1, 1e6);
uint256 lToken1Amt = bound(aToken1Amt, 1, 6e6);

// act - does not revert in this case, but instead just returns 1e18
uint256 lPrice = StableOracleMath.calcSpotPrice(_defaultAmp, lToken0Amt, lToken1Amt);

// assert
assertEq(lPrice, 1e18);
}

function testCalculatedSpotPriceIsCloseToEstimated(uint256 aReserve0, uint256 aReserve1) external {
// assume
uint256 lReserve0 = bound(aReserve0, 1e18, 1000e18);
uint256 lReserve1 = bound(aReserve1, 1e18, 1000e18);

// act
uint256 lSpotEstimated = estimateSpotPrice(lReserve0, lReserve1, 1, 1, _defaultAmp * 2);
uint256 lSpotCalculated = StableOracleMath.calcSpotPrice(_defaultAmp, lReserve0, lReserve1);

// assert
assertApproxEqRel(lSpotEstimated, lSpotCalculated, 0.000001e18); // 1% of 1bp, or a 0.0001% error
}
}

0 comments on commit 39d094b

Please sign in to comment.