Skip to content

Commit

Permalink
(Quoter) Avoid IR (#93)
Browse files Browse the repository at this point in the history
* avoid stack too deep

* pack local variables into structs; remove need for IR

* reorg struct

* snapshots

* forge fmt

* restore settings

* remove IR

* ensure tokens are ordered properly by using salts

* gas snapshot

* remove console logs
saucepoint authored Feb 17, 2024
1 parent 63d64fc commit e40e7f0
Showing 35 changed files with 137 additions and 114 deletions.
2 changes: 1 addition & 1 deletion .forge-snapshots/FullOracleObserve0After5Seconds.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2687
1922
2 changes: 1 addition & 1 deletion .forge-snapshots/FullOracleObserve200By13.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
22933
20282
2 changes: 1 addition & 1 deletion .forge-snapshots/FullOracleObserve200By13Plus5.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
23180
20520
2 changes: 1 addition & 1 deletion .forge-snapshots/FullOracleObserve5After5Seconds.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2738
2034
2 changes: 1 addition & 1 deletion .forge-snapshots/FullOracleObserveOldest.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
21892
19330
2 changes: 1 addition & 1 deletion .forge-snapshots/FullOracleObserveOldestAfter5Seconds.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
22191
19612
2 changes: 1 addition & 1 deletion .forge-snapshots/FullOracleObserveZero.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2070
1483
2 changes: 1 addition & 1 deletion .forge-snapshots/FullRangeAddInitialLiquidity.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
407968
393062
2 changes: 1 addition & 1 deletion .forge-snapshots/FullRangeAddLiquidity.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
201962
187418
2 changes: 1 addition & 1 deletion .forge-snapshots/FullRangeFirstSwap.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
153306
136762
2 changes: 1 addition & 1 deletion .forge-snapshots/FullRangeInitialize.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1112212
1059719
2 changes: 1 addition & 1 deletion .forge-snapshots/FullRangeRemoveLiquidity.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
197519
180886
2 changes: 1 addition & 1 deletion .forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
379147
373831
2 changes: 1 addition & 1 deletion .forge-snapshots/FullRangeSecondSwap.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
111940
97479
2 changes: 1 addition & 1 deletion .forge-snapshots/FullRangeSwap.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
151523
135037
2 changes: 1 addition & 1 deletion .forge-snapshots/OracleGrow10Slots.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
254660
232968
2 changes: 1 addition & 1 deletion .forge-snapshots/OracleGrow10SlotsCardinalityGreater.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
245360
223657
2 changes: 1 addition & 1 deletion .forge-snapshots/OracleGrow1Slot.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
54869
32853
2 changes: 1 addition & 1 deletion .forge-snapshots/OracleGrow1SlotCardinalityGreater.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
45569
23553
2 changes: 1 addition & 1 deletion .forge-snapshots/OracleInitialize.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
72316
51321
Original file line number Diff line number Diff line change
@@ -1 +1 @@
6492
5397
2 changes: 1 addition & 1 deletion .forge-snapshots/OracleObserveCurrentTime.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2070
1483
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2070
1483
2 changes: 1 addition & 1 deletion .forge-snapshots/OracleObserveLast20Seconds.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
86878
73451
2 changes: 1 addition & 1 deletion .forge-snapshots/OracleObserveLatestEqual.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2070
1483
2 changes: 1 addition & 1 deletion .forge-snapshots/OracleObserveLatestTransform.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2687
1922
2 changes: 1 addition & 1 deletion .forge-snapshots/OracleObserveMiddle.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
6684
5572
2 changes: 1 addition & 1 deletion .forge-snapshots/OracleObserveOldest.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
6193
5115
2 changes: 1 addition & 1 deletion .forge-snapshots/OracleObserveSinceMostRecent.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3382
2537
2 changes: 1 addition & 1 deletion .forge-snapshots/TWAMMSubmitOrder.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
145648
122817
16 changes: 6 additions & 10 deletions contracts/hooks/examples/LimitOrder.sol
Original file line number Diff line number Diff line change
@@ -299,8 +299,6 @@ contract LimitOrder is BaseHook {
uint128 liquidity = epochInfo.liquidity[msg.sender];
if (liquidity == 0) revert ZeroLiquidity();
delete epochInfo.liquidity[msg.sender];
uint128 liquidityTotal = epochInfo.liquidityTotal;
epochInfo.liquidityTotal = liquidityTotal - liquidity;

uint256 amount0Fee;
uint256 amount1Fee;
@@ -309,12 +307,12 @@ contract LimitOrder is BaseHook {
address(this),
abi.encodeCall(
this.lockAcquiredKill,
(key, tickLower, -int256(uint256(liquidity)), to, liquidity == liquidityTotal)
(key, tickLower, -int256(uint256(liquidity)), to, liquidity == epochInfo.liquidityTotal)
)
),
(uint256, uint256, uint256, uint256)
);

epochInfo.liquidityTotal -= liquidity;
unchecked {
epochInfo.token0Total += amount0Fee;
epochInfo.token1Total += amount1Fee;
@@ -378,15 +376,13 @@ contract LimitOrder is BaseHook {
if (liquidity == 0) revert ZeroLiquidity();
delete epochInfo.liquidity[msg.sender];

uint256 token0Total = epochInfo.token0Total;
uint256 token1Total = epochInfo.token1Total;
uint128 liquidityTotal = epochInfo.liquidityTotal;

amount0 = FullMath.mulDiv(token0Total, liquidity, liquidityTotal);
amount1 = FullMath.mulDiv(token1Total, liquidity, liquidityTotal);
amount0 = FullMath.mulDiv(epochInfo.token0Total, liquidity, liquidityTotal);
amount1 = FullMath.mulDiv(epochInfo.token1Total, liquidity, liquidityTotal);

epochInfo.token0Total = token0Total - amount0;
epochInfo.token1Total = token1Total - amount1;
epochInfo.token0Total -= amount0;
epochInfo.token1Total -= amount1;
epochInfo.liquidityTotal = liquidityTotal - liquidity;

poolManager.lock(
107 changes: 65 additions & 42 deletions contracts/lens/Quoter.sol
Original file line number Diff line number Diff line change
@@ -29,6 +29,23 @@ contract Quoter is IQuoter, ILockCallback {
/// @dev int128[2] + sqrtPriceX96After padded to 32bytes + intializeTicksLoaded padded to 32bytes
uint256 internal constant MINIMUM_VALID_RESPONSE_LENGTH = 96;

struct QuoteResult {
int128[] deltaAmounts;
uint160[] sqrtPriceX96AfterList;
uint32[] initializedTicksLoadedList;
}

struct QuoteCache {
BalanceDelta curDeltas;
uint128 prevAmount;
int128 deltaIn;
int128 deltaOut;
int24 tickBefore;
int24 tickAfter;
Currency prevCurrency;
uint160 sqrtPriceX96After;
}

/// @dev Only this address may call this function
modifier selfOnly() {
if (msg.sender != address(this)) revert NotSelf();
@@ -151,39 +168,42 @@ contract Quoter is IQuoter, ILockCallback {
function _quoteExactInput(QuoteExactParams memory params) public selfOnly returns (bytes memory) {
uint256 pathLength = params.path.length;

int128[] memory deltaAmounts = new int128[](pathLength + 1);
uint160[] memory sqrtPriceX96AfterList = new uint160[](pathLength);
uint32[] memory initializedTicksLoadedList = new uint32[](pathLength);
Currency prevCurrencyOut;
uint128 prevAmountOut;
QuoteResult memory result = QuoteResult({
deltaAmounts: new int128[](pathLength + 1),
sqrtPriceX96AfterList: new uint160[](pathLength),
initializedTicksLoadedList: new uint32[](pathLength)
});
QuoteCache memory cache;

for (uint256 i = 0; i < pathLength; i++) {
(PoolKey memory poolKey, bool zeroForOne) =
params.path[i].getPoolAndSwapDirection(i == 0 ? params.exactCurrency : prevCurrencyOut);
(, int24 tickBefore,) = manager.getSlot0(poolKey.toId());
params.path[i].getPoolAndSwapDirection(i == 0 ? params.exactCurrency : cache.prevCurrency);
(, cache.tickBefore,) = manager.getSlot0(poolKey.toId());

(BalanceDelta curDeltas, uint160 sqrtPriceX96After, int24 tickAfter) = _swap(
(cache.curDeltas, cache.sqrtPriceX96After, cache.tickAfter) = _swap(
poolKey,
zeroForOne,
int256(int128(i == 0 ? params.exactAmount : prevAmountOut)),
int256(int128(i == 0 ? params.exactAmount : cache.prevAmount)),
0,
params.path[i].hookData
);

(int128 deltaIn, int128 deltaOut) =
zeroForOne ? (curDeltas.amount0(), curDeltas.amount1()) : (curDeltas.amount1(), curDeltas.amount0());
deltaAmounts[i] += deltaIn;
deltaAmounts[i + 1] += deltaOut;

prevAmountOut = zeroForOne ? uint128(-curDeltas.amount1()) : uint128(-curDeltas.amount0());
prevCurrencyOut = params.path[i].intermediateCurrency;
sqrtPriceX96AfterList[i] = sqrtPriceX96After;
initializedTicksLoadedList[i] =
PoolTicksCounter.countInitializedTicksLoaded(manager, poolKey, tickBefore, tickAfter);
(cache.deltaIn, cache.deltaOut) = zeroForOne
? (cache.curDeltas.amount0(), cache.curDeltas.amount1())
: (cache.curDeltas.amount1(), cache.curDeltas.amount0());
result.deltaAmounts[i] += cache.deltaIn;
result.deltaAmounts[i + 1] += cache.deltaOut;

cache.prevAmount = zeroForOne ? uint128(-cache.curDeltas.amount1()) : uint128(-cache.curDeltas.amount0());
cache.prevCurrency = params.path[i].intermediateCurrency;
result.sqrtPriceX96AfterList[i] = cache.sqrtPriceX96After;
result.initializedTicksLoadedList[i] =
PoolTicksCounter.countInitializedTicksLoaded(manager, poolKey, cache.tickBefore, cache.tickAfter);
}
bytes memory result = abi.encode(deltaAmounts, sqrtPriceX96AfterList, initializedTicksLoadedList);
bytes memory r =
abi.encode(result.deltaAmounts, result.sqrtPriceX96AfterList, result.initializedTicksLoadedList);
assembly {
revert(add(0x20, result), mload(result))
revert(add(0x20, r), mload(r))
}
}

@@ -216,42 +236,45 @@ contract Quoter is IQuoter, ILockCallback {
function _quoteExactOutput(QuoteExactParams memory params) public selfOnly returns (bytes memory) {
uint256 pathLength = params.path.length;

int128[] memory deltaAmounts = new int128[](pathLength + 1);
uint160[] memory sqrtPriceX96AfterList = new uint160[](pathLength);
uint32[] memory initializedTicksLoadedList = new uint32[](pathLength);
Currency prevCurrencyIn;
uint128 prevAmountIn;
QuoteResult memory result = QuoteResult({
deltaAmounts: new int128[](pathLength + 1),
sqrtPriceX96AfterList: new uint160[](pathLength),
initializedTicksLoadedList: new uint32[](pathLength)
});
QuoteCache memory cache;
uint128 curAmountOut;

for (uint256 i = pathLength; i > 0; i--) {
curAmountOut = i == pathLength ? params.exactAmount : prevAmountIn;
curAmountOut = i == pathLength ? params.exactAmount : cache.prevAmount;
amountOutCached = curAmountOut;

(PoolKey memory poolKey, bool oneForZero) = PathKeyLib.getPoolAndSwapDirection(
params.path[i - 1], i == pathLength ? params.exactCurrency : prevCurrencyIn
params.path[i - 1], i == pathLength ? params.exactCurrency : cache.prevCurrency
);

(, int24 tickBefore,) = manager.getSlot0(poolKey.toId());
(, cache.tickBefore,) = manager.getSlot0(poolKey.toId());

(BalanceDelta curDeltas, uint160 sqrtPriceX96After, int24 tickAfter) =
(cache.curDeltas, cache.sqrtPriceX96After, cache.tickAfter) =
_swap(poolKey, !oneForZero, -int256(uint256(curAmountOut)), 0, params.path[i - 1].hookData);

// always clear because sqrtPriceLimitX96 is set to 0 always
delete amountOutCached;
(int128 deltaIn, int128 deltaOut) =
!oneForZero ? (curDeltas.amount0(), curDeltas.amount1()) : (curDeltas.amount1(), curDeltas.amount0());
deltaAmounts[i - 1] += deltaIn;
deltaAmounts[i] += deltaOut;

prevAmountIn = !oneForZero ? uint128(curDeltas.amount0()) : uint128(curDeltas.amount1());
prevCurrencyIn = params.path[i - 1].intermediateCurrency;
sqrtPriceX96AfterList[i - 1] = sqrtPriceX96After;
initializedTicksLoadedList[i - 1] =
PoolTicksCounter.countInitializedTicksLoaded(manager, poolKey, tickBefore, tickAfter);
(cache.deltaIn, cache.deltaOut) = !oneForZero
? (cache.curDeltas.amount0(), cache.curDeltas.amount1())
: (cache.curDeltas.amount1(), cache.curDeltas.amount0());
result.deltaAmounts[i - 1] += cache.deltaIn;
result.deltaAmounts[i] += cache.deltaOut;

cache.prevAmount = !oneForZero ? uint128(cache.curDeltas.amount0()) : uint128(cache.curDeltas.amount1());
cache.prevCurrency = params.path[i - 1].intermediateCurrency;
result.sqrtPriceX96AfterList[i - 1] = cache.sqrtPriceX96After;
result.initializedTicksLoadedList[i - 1] =
PoolTicksCounter.countInitializedTicksLoaded(manager, poolKey, cache.tickBefore, cache.tickAfter);
}
bytes memory result = abi.encode(deltaAmounts, sqrtPriceX96AfterList, initializedTicksLoadedList);
bytes memory r =
abi.encode(result.deltaAmounts, result.sqrtPriceX96AfterList, result.initializedTicksLoadedList);
assembly {
revert(add(0x20, result), mload(result))
revert(add(0x20, r), mload(r))
}
}

54 changes: 29 additions & 25 deletions contracts/libraries/PoolTicksCounter.sol
Original file line number Diff line number Diff line change
@@ -9,6 +9,15 @@ import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol";
library PoolTicksCounter {
using PoolIdLibrary for PoolKey;

struct TickCache {
int16 wordPosLower;
int16 wordPosHigher;
uint8 bitPosLower;
uint8 bitPosHigher;
bool tickBeforeInitialized;
bool tickAfterInitialized;
}

/// @dev This function counts the number of initialized ticks that would incur a gas cost between tickBefore and tickAfter.
/// When tickBefore and/or tickAfter themselves are initialized, the logic over whether we should count them depends on the
/// direction of the swap. If we are swapping upwards (tickAfter > tickBefore) we don't want to count tickBefore but we do
@@ -18,12 +27,7 @@ library PoolTicksCounter {
view
returns (uint32 initializedTicksLoaded)
{
int16 wordPosLower;
int16 wordPosHigher;
uint8 bitPosLower;
uint8 bitPosHigher;
bool tickBeforeInitialized;
bool tickAfterInitialized;
TickCache memory cache;

{
// Get the key and offset in the tick bitmap of the active tick before and after the swap.
@@ -39,53 +43,53 @@ library PoolTicksCounter {
// and we shouldn't count it.
uint256 bmAfter = self.getPoolBitmapInfo(key.toId(), wordPosAfter);
//uint256 bmAfter = PoolGetters.getTickBitmapAtWord(self, key.toId(), wordPosAfter);
tickAfterInitialized =
cache.tickAfterInitialized =
((bmAfter & (1 << bitPosAfter)) > 0) && ((tickAfter % key.tickSpacing) == 0) && (tickBefore > tickAfter);

// In the case where tickBefore is initialized, we only want to count it if we are swapping upwards.
// Use the same logic as above to decide whether we should count tickBefore or not.
uint256 bmBefore = self.getPoolBitmapInfo(key.toId(), wordPos);
//uint256 bmBefore = PoolGetters.getTickBitmapAtWord(self, key.toId(), wordPos);
tickBeforeInitialized =
cache.tickBeforeInitialized =
((bmBefore & (1 << bitPos)) > 0) && ((tickBefore % key.tickSpacing) == 0) && (tickBefore < tickAfter);

if (wordPos < wordPosAfter || (wordPos == wordPosAfter && bitPos <= bitPosAfter)) {
wordPosLower = wordPos;
bitPosLower = bitPos;
wordPosHigher = wordPosAfter;
bitPosHigher = bitPosAfter;
cache.wordPosLower = wordPos;
cache.bitPosLower = bitPos;
cache.wordPosHigher = wordPosAfter;
cache.bitPosHigher = bitPosAfter;
} else {
wordPosLower = wordPosAfter;
bitPosLower = bitPosAfter;
wordPosHigher = wordPos;
bitPosHigher = bitPos;
cache.wordPosLower = wordPosAfter;
cache.bitPosLower = bitPosAfter;
cache.wordPosHigher = wordPos;
cache.bitPosHigher = bitPos;
}
}

// Count the number of initialized ticks crossed by iterating through the tick bitmap.
// Our first mask should include the lower tick and everything to its left.
uint256 mask = type(uint256).max << bitPosLower;
while (wordPosLower <= wordPosHigher) {
uint256 mask = type(uint256).max << cache.bitPosLower;
while (cache.wordPosLower <= cache.wordPosHigher) {
// If we're on the final tick bitmap page, ensure we only count up to our
// ending tick.
if (wordPosLower == wordPosHigher) {
mask = mask & (type(uint256).max >> (255 - bitPosHigher));
if (cache.wordPosLower == cache.wordPosHigher) {
mask = mask & (type(uint256).max >> (255 - cache.bitPosHigher));
}

//uint256 bmLower = PoolGetters.getTickBitmapAtWord(self, key.toId(), wordPosLower);
uint256 bmLower = self.getPoolBitmapInfo(key.toId(), wordPosLower);
//uint256 bmLower = PoolGetters.getTickBitmapAtWord(self, key.toId(), cache.wordPosLower);
uint256 bmLower = self.getPoolBitmapInfo(key.toId(), cache.wordPosLower);
uint256 masked = bmLower & mask;
initializedTicksLoaded += countOneBits(masked);
wordPosLower++;
cache.wordPosLower++;
// Reset our mask so we consider all bits on the next iteration.
mask = type(uint256).max;
}

if (tickAfterInitialized) {
if (cache.tickAfterInitialized) {
initializedTicksLoaded -= 1;
}

if (tickBeforeInitialized) {
if (cache.tickBeforeInitialized) {
initializedTicksLoaded -= 1;
}

1 change: 0 additions & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
@@ -2,7 +2,6 @@
src = 'contracts'
out = 'foundry-out'
solc_version = '0.8.20'
via_ir = true
optimizer_runs = 1000000
ffi = true
fs_permissions = [{ access = "read-write", path = ".forge-snapshots/"}]
13 changes: 7 additions & 6 deletions test/Quoter.t.sol
Original file line number Diff line number Diff line change
@@ -53,14 +53,15 @@ contract QuoterTest is Test, Deployers {
quoter = new Quoter(address(manager));
positionManager = new PoolModifyPositionTest(manager);

// salts are chosen so that address(token0) < address(token2) && address(1) < address(token2)
bytes32 salt1 = "ffff";
bytes32 salt2 = "gm";
token0 = new MockERC20{salt: salt1}("Test0", "0", 18);
// salts are chosen so that address(token0) < address(token1) && address(token1) < address(token2)
bytes32 salt0 = "1234";
bytes32 salt1 = "gm uniswap";
bytes32 salt2 = "ffff";
token0 = new MockERC20{salt: salt0}("Test0", "0", 18);
token0.mint(address(this), 2 ** 128);
token1 = new MockERC20{salt: salt2}("Test1", "1", 18);
token1 = new MockERC20{salt: salt1}("Test1", "1", 18);
token1.mint(address(this), 2 ** 128);
token2 = new MockERC20("Test2", "2", 18);
token2 = new MockERC20{salt: salt2}("Test2", "2", 18);
token2.mint(address(this), 2 ** 128);

key01 = createPoolKey(token0, token1, address(0));

0 comments on commit e40e7f0

Please sign in to comment.