I want to calculate logarithm of a non-standard base at low gas cost. First thought is to use change of base rule with log2() fixed point methods mentioned in this thread.
However Uniswap v3 core has custom code for log to the base √1.0001. It has assembly level optimizations which I'm trying to understand. However implementation is opaque and not well documented.
As per my understanding:
- First find MSB: first set of assembly code probably does this. Why is
rneeded? - Use MSB to get log2: What does the second set of assembly do after
log2is found? - Change of base rule: What is the significance of
255738958999603826347141inlog_2 * 255738958999603826347141? - What is the significance of subtracting
3402992956809132418596140100660247210fortickLowand adding291339464771989622907027621153398088495fortickHi?
TickMath.sol
/// @notice Calculates the greatest tick value such that getRatioAtTick(tick) <= ratio
/// @dev Throws in case sqrtPriceX96 < MIN_SQRT_RATIO, as MIN_SQRT_RATIO is the lowest value getRatioAtTick may
/// ever return.
/// @param sqrtPriceX96 The sqrt ratio for which to compute the tick as a Q64.96
/// @return tick The greatest tick for which the ratio is less than or equal to the input ratio
function getTickAtSqrtRatio(uint160 sqrtPriceX96) internal pure returns (int24 tick) {
// second inequality must be < because the price can never reach the price at the max tick
require(sqrtPriceX96 >= MIN_SQRT_RATIO && sqrtPriceX96 < MAX_SQRT_RATIO, 'R');
uint256 ratio = uint256(sqrtPriceX96) << 32;
uint256 r = ratio;
uint256 msb = 0;
assembly {
let f := shl(7, gt(r, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF))
msb := or(msb, f)
r := shr(f, r)
}
assembly {
let f := shl(6, gt(r, 0xFFFFFFFFFFFFFFFF))
msb := or(msb, f)
r := shr(f, r)
}
assembly {
let f := shl(5, gt(r, 0xFFFFFFFF))
msb := or(msb, f)
r := shr(f, r)
}
assembly {
let f := shl(4, gt(r, 0xFFFF))
msb := or(msb, f)
r := shr(f, r)
}
assembly {
let f := shl(3, gt(r, 0xFF))
msb := or(msb, f)
r := shr(f, r)
}
assembly {
let f := shl(2, gt(r, 0xF))
msb := or(msb, f)
r := shr(f, r)
}
assembly {
let f := shl(1, gt(r, 0x3))
msb := or(msb, f)
r := shr(f, r)
}
assembly {
let f := gt(r, 0x1)
msb := or(msb, f)
}
if (msb >= 128) r = ratio >> (msb - 127);
else r = ratio << (127 - msb);
int256 log_2 = (int256(msb) - 128) << 64;
assembly {
r := shr(127, mul(r, r))
let f := shr(128, r)
log_2 := or(log_2, shl(63, f))
r := shr(f, r)
}
assembly {
r := shr(127, mul(r, r))
let f := shr(128, r)
log_2 := or(log_2, shl(62, f))
r := shr(f, r)
}
assembly {
r := shr(127, mul(r, r))
let f := shr(128, r)
log_2 := or(log_2, shl(61, f))
r := shr(f, r)
}
assembly {
r := shr(127, mul(r, r))
let f := shr(128, r)
log_2 := or(log_2, shl(60, f))
r := shr(f, r)
}
assembly {
r := shr(127, mul(r, r))
let f := shr(128, r)
log_2 := or(log_2, shl(59, f))
r := shr(f, r)
}
assembly {
r := shr(127, mul(r, r))
let f := shr(128, r)
log_2 := or(log_2, shl(58, f))
r := shr(f, r)
}
assembly {
r := shr(127, mul(r, r))
let f := shr(128, r)
log_2 := or(log_2, shl(57, f))
r := shr(f, r)
}
assembly {
r := shr(127, mul(r, r))
let f := shr(128, r)
log_2 := or(log_2, shl(56, f))
r := shr(f, r)
}
assembly {
r := shr(127, mul(r, r))
let f := shr(128, r)
log_2 := or(log_2, shl(55, f))
r := shr(f, r)
}
assembly {
r := shr(127, mul(r, r))
let f := shr(128, r)
log_2 := or(log_2, shl(54, f))
r := shr(f, r)
}
assembly {
r := shr(127, mul(r, r))
let f := shr(128, r)
log_2 := or(log_2, shl(53, f))
r := shr(f, r)
}
assembly {
r := shr(127, mul(r, r))
let f := shr(128, r)
log_2 := or(log_2, shl(52, f))
r := shr(f, r)
}
assembly {
r := shr(127, mul(r, r))
let f := shr(128, r)
log_2 := or(log_2, shl(51, f))
r := shr(f, r)
}
assembly {
r := shr(127, mul(r, r))
let f := shr(128, r)
log_2 := or(log_2, shl(50, f))
}
int256 log_sqrt10001 = log_2 * 255738958999603826347141; // 128.128 number
int24 tickLow = int24((log_sqrt10001 - 3402992956809132418596140100660247210) >> 128);
int24 tickHi = int24((log_sqrt10001 + 291339464771989622907027621153398088495) >> 128);
tick = tickLow == tickHi ? tickLow : getSqrtRatioAtTick(tickHi) <= sqrtPriceX96 ? tickHi : tickLow;
}
3402992956809132418596140100660247210 = 0.010000497 x 2^128and291339464771989622907027621153398088495 = 0.856 x 2^128.tickshould not be fractional, so they try to find the closest whole number tick. The sum of these numbers is less than 1, to avoid skipping of a tick. But why0.01and0.856and not0.2and0.7? Not sure. Meaning of these numbers is the final piece. – secretshardul Nov 20 '21 at 04:05tickLow -= 0.01,tickHi += (accuracy_limit + 0.01) = (2^-14 + 0.01) = 0.856. An older commit using 7 digit refinement andP = 0.01^tin place of√P = 0.0001^tsatisfies this condition, i.e. 188609930776236967625146103726555653720 / 2^128 = 0.5542 = 2^-7 / log2(1.01) + 0.01`. I could be wrong though. Awaiting official word at https://github.com/Uniswap/v3-core/issues/500 – secretshardul Nov 22 '21 at 04:46(2^-14 + 0.01) = 0.856? 2^-14 + 0.01 is roughly equal to 0.01 given how small 2^-14 is. Maybe I misunderstood, though. So you are saying that the rationale for 0.01 is basically that it is an historical value that used to work ? – hroussille Nov 22 '21 at 08:242^-14 / log2(√1.0001), not2^-14as mentioned in tickHi. 0.01 could refer to a 1% error margin? – secretshardul Nov 22 '21 at 09:57