Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Price Impact Helper #323

Merged
merged 29 commits into from
Jan 6, 2025
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a238315
Add standalone-utils with PriceImpact helper contract (wip)
brunoguerios Feb 22, 2024
5c06a0a
Apply suggestions from hardhat solidity extension on vscode
brunoguerios Feb 26, 2024
447366e
Add .vscode to gitignore
brunoguerios Feb 26, 2024
b51856b
Apply code review suggestions
brunoguerios Feb 27, 2024
21dbcbf
Add test (wip)
brunoguerios Feb 27, 2024
bbd553c
Fix NotStaticCall error from tests
brunoguerios Feb 28, 2024
daa6954
Implement helper, tests.
Mar 13, 2024
7f7a52c
Remove unnecessary error.
Mar 13, 2024
fbfc101
Merge branch 'main' into clean-quote
Mar 13, 2024
682257d
Merge branch 'clean-quote' into price-impact-helper
Mar 13, 2024
2802842
Use try catch instead of raw call.
Mar 13, 2024
5845a1e
Merge branch 'clean-quote' into price-impact-helper
Mar 13, 2024
558820b
Update to use queryAndRevert
brunoguerios Mar 19, 2024
61c20b6
Fix lint
brunoguerios Mar 20, 2024
af7e093
Improve tests
brunoguerios Mar 22, 2024
24f81e8
Merge branch 'main' into price-impact-helper
EndymionJkb Dec 9, 2024
4e796a0
refactor: update to new signatures
EndymionJkb Dec 9, 2024
ab5c279
fix repo configs
elshan-eth Dec 9, 2024
55b596f
fix sol versions
elshan-eth Dec 9, 2024
8ce8373
refactor
elshan-eth Dec 10, 2024
cd8aaf7
add unit tests
elshan-eth Dec 13, 2024
e1d0a70
fix calls and add tests
elshan-eth Dec 13, 2024
28ba108
Delete pkg/standalone-utils/CHANGELOG.md
joaobrunoah Dec 30, 2024
c31fc64
Merge branch 'main' into price-impact-helper
joaobrunoah Dec 30, 2024
3354423
SafeCast
joaobrunoah Jan 2, 2025
7d847ac
Fix variable and add comment
joaobrunoah Jan 2, 2025
36e15f1
Merge branch 'main' into price-impact-helper
joaobrunoah Jan 2, 2025
b98b81f
Merge branch 'main' into price-impact-helper
joaobrunoah Jan 3, 2025
f991bea
Fix price impact test
joaobrunoah Jan 3, 2025
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
3 changes: 3 additions & 0 deletions pkg/standalone-utils/.solcover.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
skipFiles: ['test'],
};
1 change: 1 addition & 0 deletions pkg/standalone-utils/.solhintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
contracts/test/
3 changes: 3 additions & 0 deletions pkg/standalone-utils/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Changelog

## Unreleased
15 changes: 15 additions & 0 deletions pkg/standalone-utils/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# <img src="../../logo.svg" alt="Balancer" height="128px">

# Balancer V3 Standalone Utils

This package contains standalone utilities that can be used to perform advanced actions in the Balancer V3 protocol.

- [`PriceImpact`](./contracts/PriceImpact.sol) can be used by off-chain clients to calculate price impact for add liquidity unbalanced operations.
elshan-eth marked this conversation as resolved.
Show resolved Hide resolved

## Overview

### Usage

## Licensing

[GNU General Public License Version 3 (GPL v3)](../../LICENSE).
39 changes: 39 additions & 0 deletions pkg/standalone-utils/contracts/CallAndRevert.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: GPL-3.0-or-later

pragma solidity ^0.8.24;

import { Address } from "@openzeppelin/contracts/utils/Address.sol";

import { RevertCodec } from "@balancer-labs/v3-solidity-utils/contracts/helpers/RevertCodec.sol";

contract CallAndRevert {
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe we can make a library out of this? There's no state to save or to access, so a library would seem better suited. Agree about making it a standalone module.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah this is to reuse the hook right? Then I guess it's fine as it is.

error QuoteResultSpoofed();

function _callAndRevert(address target, bytes memory data) internal returns (bytes memory) {
try CallAndRevert(address(this)).callAndRevertHook(target, data) {
revert("Unexpected success");
} catch (bytes memory result) {
return RevertCodec.catchEncodedResult(result);
}
}

function callAndRevertHook(address target, bytes memory data) external returns (uint256) {
// solhint-disable-next-line avoid-low-level-calls
(bool success, bytes memory result) = (target).call(data);
if (success) {
// This will only revert if result is empty and sender account has no code.
Address.verifyCallResultFromTarget(msg.sender, success, result);
// Send result in revert reason.
revert RevertCodec.Result(result);
} else {
// If the call reverted with a spoofed `QuoteResult`, we catch it and bubble up a different reason.
bytes4 errorSelector = RevertCodec.parseSelector(result);
if (errorSelector == RevertCodec.Result.selector) {
revert QuoteResultSpoofed();
}

// Otherwise we bubble up the original revert reason.
RevertCodec.bubbleUpRevert(result);
}
}
}
251 changes: 251 additions & 0 deletions pkg/standalone-utils/contracts/PriceImpactHelper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
// SPDX-License-Identifier: GPL-3.0-or-later

pragma solidity ^0.8.24;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol";
import { IRouter } from "@balancer-labs/v3-interfaces/contracts/vault/IRouter.sol";
import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol";

import { CallAndRevert } from "./CallAndRevert.sol";

contract PriceImpactHelper is CallAndRevert {
using FixedPoint for uint256;

IVault internal immutable _vault;
IRouter internal immutable _router;

constructor(IVault vault, IRouter router) {
_vault = vault;
_router = router;
}

/*******************************************************************************
Price Impact
*******************************************************************************/

function calculateAddLiquidityUnbalancedPriceImpact(
address pool,
uint256[] memory exactAmountsIn,
address sender
) external returns (uint256 priceImpact) {
uint256 bptAmountOut = _queryAddLiquidityUnbalanced(pool, exactAmountsIn, sender);
uint256[] memory proportionalAmountsOut = _queryRemoveLiquidityProportional(pool, bptAmountOut, sender);

// get deltas between exactAmountsIn and proportionalAmountsOut
int256[] memory deltas = new int256[](exactAmountsIn.length);
for (uint256 i = 0; i < exactAmountsIn.length; i++) {
deltas[i] = int(proportionalAmountsOut[i]) - int(exactAmountsIn[i]);
joaobrunoah marked this conversation as resolved.
Show resolved Hide resolved
}

// query add liquidity for each delta, so we know how unbalanced each amount in is in terms of BPT
int256[] memory deltaBPTs = new int256[](exactAmountsIn.length);
for (uint256 i = 0; i < exactAmountsIn.length; i++) {
deltaBPTs[i] = _queryAddLiquidityUnbalancedForTokenDeltas(pool, i, deltas, sender);
}

// zero out deltas leaving only a remaining delta within a single token
uint256 remainingDeltaIndex = _zeroOutDeltas(pool, deltas, deltaBPTs, sender);

// calculate price impact ABA with remaining delta and its respective exactAmountIn
// remaining delta is always negative, so by multiplying by -1 we get a positive number
uint256 delta = uint(-deltas[remainingDeltaIndex]);
joaobrunoah marked this conversation as resolved.
Show resolved Hide resolved
return delta.divDown(exactAmountsIn[remainingDeltaIndex]) / 2;
}

/*******************************************************************************
Router Queries
*******************************************************************************/

function _queryAddLiquidityUnbalanced(
address pool,
uint256[] memory exactAmountsIn,
address sender
) internal returns (uint256) {
return
abi.decode(
_callAndRevert(
address(_router),
abi.encodeWithSelector(
_router.queryAddLiquidityUnbalanced.selector,
pool,
exactAmountsIn,
sender,
""
)
),
(uint256)
);
}

function _queryRemoveLiquidityProportional(
address pool,
uint256 bptAmountOut,
address sender
) internal returns (uint256[] memory) {
return
abi.decode(
_callAndRevert(
address(_router),
abi.encodeWithSelector(
_router.queryRemoveLiquidityProportional.selector,
pool,
bptAmountOut,
sender,
""
)
),
(uint256[])
);
}

function _querySwapSingleTokenExactIn(
address pool,
IERC20 tokenIn,
IERC20 tokenOut,
uint256 amountIn,
address sender
) internal returns (uint256) {
return
abi.decode(
_callAndRevert(
address(_router),
abi.encodeWithSelector(
_router.querySwapSingleTokenExactIn.selector,
pool,
tokenIn,
tokenOut,
amountIn,
sender,
""
)
),
(uint256)
);
}

function _querySwapSingleTokenExactOut(
address pool,
IERC20 tokenIn,
IERC20 tokenOut,
uint256 amountOut,
address sender
) internal returns (uint256) {
return
abi.decode(
_callAndRevert(
address(_router),
abi.encodeWithSelector(
_router.querySwapSingleTokenExactOut.selector,
pool,
tokenIn,
tokenOut,
amountOut,
sender,
""
)
),
(uint256)
);
}

/*******************************************************************************
Helpers
*******************************************************************************/

function _queryAddLiquidityUnbalancedForTokenDeltas(
address pool,
uint256 tokenIndex,
int256[] memory deltas,
address sender
) internal returns (int256 deltaBPT) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think I don't get the back and forth here. Why is this a signed value if we always change the result to be positive?

Copy link
Contributor

@joaobrunoah joaobrunoah Jan 2, 2025

Choose a reason for hiding this comment

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

It took me a while to see, but we check if delta is positive or negative, and then we return result or -result. The returned value can be negative.

uint256[] memory zerosWithSingleDelta = new uint256[](deltas.length);
int256 delta = deltas[tokenIndex];

if (delta == 0) {
return 0;
}

zerosWithSingleDelta[tokenIndex] = uint256(delta > 0 ? delta : -delta);
int256 result = int256(_queryAddLiquidityUnbalanced(pool, zerosWithSingleDelta, sender));

return delta > 0 ? result : -result;
}

function _zeroOutDeltas(
address pool,
int256[] memory deltas,
int256[] memory deltaBPTs,
address sender
) internal returns (uint256) {
uint256 minNegativeDeltaIndex = 0;
IERC20[] memory poolTokens = _vault.getPoolTokens(pool);

for (uint256 i = 0; i < deltas.length - 1; i++) {
// get minPositiveDeltaIndex and maxNegativeDeltaIndex
uint256 minPositiveDeltaIndex = _minPositiveIndex(deltaBPTs);
minNegativeDeltaIndex = _maxNegativeIndex(deltaBPTs);

uint256 givenTokenIndex;
uint256 resultTokenIndex;
uint256 resultAmount;

if (deltaBPTs[minPositiveDeltaIndex] < -deltaBPTs[minNegativeDeltaIndex]) {
givenTokenIndex = minPositiveDeltaIndex;
resultTokenIndex = minNegativeDeltaIndex;
resultAmount = _querySwapSingleTokenExactIn(
pool,
poolTokens[givenTokenIndex],
poolTokens[resultTokenIndex],
uint(deltas[givenTokenIndex]),
sender
);
} else {
givenTokenIndex = minNegativeDeltaIndex;
resultTokenIndex = minPositiveDeltaIndex;
resultAmount = _querySwapSingleTokenExactOut(
pool,
poolTokens[resultTokenIndex],
poolTokens[givenTokenIndex],
uint(-deltas[givenTokenIndex]),
sender
);
}

// Update deltas and deltaBPTs
deltas[givenTokenIndex] = 0;
deltaBPTs[givenTokenIndex] = 0;
deltas[resultTokenIndex] += int(resultAmount);
deltaBPTs[resultTokenIndex] = _queryAddLiquidityUnbalancedForTokenDeltas(
pool,
resultTokenIndex,
deltas,
sender
);
}

return minNegativeDeltaIndex;
}

// returns the index of the smallest positive integer in an array - i.e. [3, 2, -2, -3] returns 1
function _minPositiveIndex(int256[] memory array) internal pure returns (uint256 index) {
int256 min = type(int256).max;
for (uint256 i = 0; i < array.length; i++) {
if (array[i] > 0 && array[i] < min) {
min = array[i];
index = i;
}
}
}

// returns the index of the biggest negative integer in an array - i.e. [3, 1, -2, -3] returns 2
function _maxNegativeIndex(int256[] memory array) internal pure returns (uint256 index) {
int256 max = type(int256).min;
for (uint256 i = 0; i < array.length; i++) {
if (array[i] < 0 && array[i] > max) {
max = array[i];
index = i;
}
}
}
}
38 changes: 38 additions & 0 deletions pkg/standalone-utils/contracts/test/PriceImpactHelperMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// SPDX-License-Identifier: GPL-3.0-or-later

pragma solidity ^0.8.24;

import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol";
import { IRouter } from "@balancer-labs/v3-interfaces/contracts/vault/IRouter.sol";

import { PriceImpactHelper } from "../PriceImpactHelper.sol";

contract PriceImpactHelperMock is PriceImpactHelper {
constructor(IVault vault, IRouter router) PriceImpactHelper(vault, router) {}

function queryAddLiquidityUnbalancedForTokenDeltas(
address pool,
uint256 tokenIndex,
int256[] memory deltas,
address sender
) external returns (int256 deltaBPT) {
return _queryAddLiquidityUnbalancedForTokenDeltas(pool, tokenIndex, deltas, sender);
}

function zeroOutDeltas(
address pool,
int256[] memory deltas,
int256[] memory deltaBPTs,
address sender
) external returns (uint256) {
return _zeroOutDeltas(pool, deltas, deltaBPTs, sender);
}

function minPositiveIndex(int256[] memory array) external pure returns (uint256) {
return _minPositiveIndex(array);
}

function maxNegativeIndex(int256[] memory array) external pure returns (uint256) {
return _maxNegativeIndex(array);
}
}
1 change: 1 addition & 0 deletions pkg/standalone-utils/coverage.sh
Loading
Loading