Skip to content
Merged
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
334 changes: 170 additions & 164 deletions spot-vaults/contracts/BillBroker.sol

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions spot-vaults/contracts/_interfaces/errors/CommonErrors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,9 @@ error UnacceptableSwap();

/// @notice Expected usable external price.
error UnreliablePrice();

/// @notice Range upper is larger than lower
error InvalidRange();

/// @notice Expected range delta to be smaller.
error UnexpectedRangeDelta();
9 changes: 5 additions & 4 deletions spot-vaults/contracts/_interfaces/types/BillBrokerTypes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ struct BillBrokerFees {
uint256 mintFeePerc;
/// @notice The percentage fee charged for burning BillBroker LP tokens.
uint256 burnFeePerc;
/// @notice Range of fee percentages for swapping from perp tokens to USD.
Range perpToUSDSwapFeePercs;
/// @notice Range of fee percentages for swapping from USD to perp tokens.
Range usdToPerpSwapFeePercs;
/// @notice Range of fee factors for swapping from perp tokens to USD.
/// @dev Factor of 1.02 implies a +2% fees, and 0.98 implies a -2% fees.
Range perpToUSDSwapFeeFactors;
/// @notice Range of fee factors for swapping from USD to perp tokens.
Range usdToPerpSwapFeeFactors;
/// @notice The percentage of the swap fees that goes to the protocol.
uint256 protocolSwapSharePerc;
}
Expand Down
31 changes: 31 additions & 0 deletions spot-vaults/contracts/_test/LineHelpersTester.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.24;

import { LineHelpers } from "../_utils/LineHelpers.sol";
import { Line, Range } from "../_interfaces/types/CommonTypes.sol";

contract LineHelpersTester {
using LineHelpers for Line;

function testComputeY(Line memory fn, uint256 x) public pure returns (int256) {
return fn.computeY(x);
}

function testAvgY(
Line memory fn,
uint256 xL,
uint256 xU
) public pure returns (int256) {
return fn.avgY(xL, xU);
}

function testComputePiecewiseAvgY(
Line memory fn1,
Line memory fn2,
Line memory fn3,
Range memory xBreakPt,
Range memory xRange
) public pure returns (int256) {
return LineHelpers.computePiecewiseAvgY(fn1, fn2, fn3, xBreakPt, xRange);
}
}
102 changes: 89 additions & 13 deletions spot-vaults/contracts/_utils/LineHelpers.sol
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.24;

import { Math } from "@openzeppelin/contracts/utils/math/Math.sol";
import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol";
import { Line } from "../_interfaces/types/CommonTypes.sol";
import { MathHelpers } from "./MathHelpers.sol";
import { Line, Range } from "../_interfaces/types/CommonTypes.sol";
import { InvalidRange, UnexpectedRangeDelta } from "../_interfaces/errors/CommonErrors.sol";

/**
* @title LineHelpers
Expand All @@ -11,7 +14,26 @@ import { Line } from "../_interfaces/types/CommonTypes.sol";
*
*/
library LineHelpers {
using Math for uint256;
using MathHelpers for uint256;
using SafeCast for uint256;
using SafeCast for int256;

/// @dev This function computes y for a given x on the line (fn).
function computeY(Line memory fn, uint256 x) internal pure returns (int256) {
// If the line has a zero slope, return any y.
if (fn.y1 == fn.y2) {
return fn.y1.toInt256();
}

// m = dlY/dlX
// c = y2 - m . x2
// y = m . x + c
int256 dlY = fn.y2.toInt256() - fn.y1.toInt256();
int256 dlX = fn.x2.toInt256() - fn.x1.toInt256();
int256 c = fn.y2.toInt256() - ((fn.x2.toInt256() * dlY) / dlX);
return (((x.toInt256() * dlY) / dlX) + c);
}

/// @dev We compute the average height of the line between {xL,xU}.
function avgY(Line memory fn, uint256 xL, uint256 xU) internal pure returns (int256) {
Expand All @@ -20,6 +42,7 @@ library LineHelpers {
return fn.y2.toInt256();
}

// NOTE: There is some precision loss because we cast to int and back
// m = dlY/dlX
// c = y2 - m . x2
// Avg height => (yL + yU) / 2
Expand All @@ -30,19 +53,72 @@ library LineHelpers {
return ((((xL + xU).toInt256() * dlY) / (2 * dlX)) + c);
}

/// @dev This function computes y for a given x on the line (fn).
function computeY(Line memory fn, uint256 x) internal pure returns (int256) {
// If the line has a zero slope, return any y.
if (fn.y1 == fn.y2) {
return fn.y1.toInt256();
/// @notice Computes a piecewise average value (yVal) over the domain xRange,
/// based on three linear segments (fn1, fn2, fn3) that switch at xBreakPt.
/// @dev The function splits the input range into up to three segments, then
/// calculates a weighted average in each segment using the corresponding
/// piecewise function.
/// @dev AI-GENERATED
/// @param fn1 Piecewise linear function used when x is below xBreakPt.lower.
/// @param fn2 Piecewise linear function used when x is between xBreakPt.lower and xBreakPt.upper.
/// @param fn3 Piecewise linear function used when x is above xBreakPt.upper.
/// @param xBreakPt Range denoting the lower and upper x thresholds.
/// @param xRange The actual x-range over which we want to compute an averaged value.
/// @return yVal The computed piecewise average.
function computePiecewiseAvgY(
Line memory fn1,
Line memory fn2,
Line memory fn3,
Range memory xBreakPt,
Range memory xRange
) internal pure returns (int256) {
int256 xl = xRange.lower.toInt256();
int256 xu = xRange.upper.toInt256();
int256 bpl = xBreakPt.lower.toInt256();
int256 bpu = xBreakPt.upper.toInt256();

// Validate range inputs (custom errors omitted here).
if (xl > xu) revert InvalidRange();
if (xl <= bpl && xu > bpu) revert UnexpectedRangeDelta();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be an error condition? The x-range could span all three line segments on a large enough trade.

This could be a case F

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be. I just chose to keep the implementation simple in the first iteration. We can add a case F in the next pass.


// ---------------------------
// CASE A: Entire xRange below xBreakPt.lower → use fn1
if (xu <= bpl) {
return avgY(fn1, xRange.lower, xRange.upper);
}

// m = dlY/dlX
// c = y2 - m . x2
// y = m . x + c
int256 dlY = fn.y2.toInt256() - fn.y1.toInt256();
int256 dlX = fn.x2.toInt256() - fn.x1.toInt256();
int256 c = fn.y2.toInt256() - ((fn.x2.toInt256() * dlY) / dlX);
return (((x.toInt256() * dlY) / dlX) + c);
// CASE B: xRange straddles bpl but still <= bpu
// Blend fn1 and fn2
if (xl <= bpl && xu <= bpu) {
// w1 = portion in fn1, w2 = portion in fn2
int256 w1 = bpl - xl;
int256 w2 = xu - bpl;
// Weighted average across two sub-ranges
return
(avgY(fn1, xRange.lower, xBreakPt.lower) *
w1 +
avgY(fn2, xBreakPt.lower, xRange.upper) *
w2) / (w1 + w2);
}

// CASE C: Fully within [bpl, bpu] → use fn2
if (xl > bpl && xu <= bpu) {
return avgY(fn2, xRange.lower, xRange.upper);
}

// CASE D: xRange straddles xBreakPt.upper → blend fn2 and fn3
if (xl <= bpu && xu > bpu) {
int256 w1 = bpu - xl;
int256 w2 = xu - bpu;
return
(avgY(fn2, xRange.lower, xBreakPt.upper) *
w1 +
avgY(fn3, xBreakPt.upper, xRange.upper) *
w2) / (w1 + w2);
}

// CASE E: Entire xRange above xBreakPt.upper → use fn3
// (if none of the above conditions matched, we must be here)
return avgY(fn3, xRange.lower, xRange.upper);
}
}
7 changes: 6 additions & 1 deletion spot-vaults/contracts/_utils/MathHelpers.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ library MathHelpers {

/// @dev Clips a given integer number between provided min and max unsigned integer.
function clip(int256 n, uint256 min, uint256 max) internal pure returns (uint256) {
return Math.min(Math.max((n >= 0) ? n.toUint256() : 0, min), max);
return clip(n > 0 ? n.toUint256() : 0, min, max);
}

/// @dev Clips a given unsigned integer between provided min and max unsigned integer.
function clip(uint256 n, uint256 min, uint256 max) internal pure returns (uint256) {
return Math.min(Math.max(n, min), max);
}
}
2 changes: 1 addition & 1 deletion spot-vaults/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
"ethers": "^6.6.0",
"ethers-v5": "npm:ethers@^5.7.0",
"ganache-cli": "latest",
"hardhat": "^2.22.10",
"hardhat": "^2.22.18",
"hardhat-gas-reporter": "latest",
"lodash": "^4.17.21",
"prettier": "^2.7.1",
Expand Down
Loading