Omniscia Steer Protocol Audit
DynamicFeeHook Manual Review Findings
DynamicFeeHook Manual Review Findings
DFH-01M: Inexplicable Tick Scaling Factor
| Type | Severity | Location |
|---|---|---|
| Code Style | ![]() | DynamicFeeHook.sol:L263 |
Description:
The tick scaling factor chosen for calculating the scaled volatility of an AMM pair is not accompanied by adequate documentation.
Example:
255/// @return volatility scaled to [0, VOL_SCALE] range256function _getScaledVol() internal view returns (uint256) {257 int24 twat = volatility.twatTick;258 int24 cur = _getCurrentTick();259
260 int24 diff = cur >= twat ? cur - twat : twat - cur;261
262 // scale ticks → pseudo-unitless integer263 return (uint256(uint24(diff)) * VOL_SCALE) / TICK_SCALING_FACTOR;264}Recommendation:
We advise proper documentation to be introduced for it, permitting its validation as a sound value.
Alleviation (5dd8944ae7):
The documentation of the system was not expanded to properly justify the presence of the tick scaling factor although changes pertaining to it were introduced.
We advise its purpose to be strictly clarified so as to permit its validation as sensitivity scaling without denoting the value's own denomination (i.e. fractional, decimal, etc.) is insufficient.
Alleviation (9711893da5):
The Steer Protocol team supplied additional documentation in the form of in-line comments surrounding the referenced values, illustrating how they are expected to be utilized.
We assessed the existing implementation with its revised documentation and have confirmed that the implementations are in alignment, rendering this exhibit addressed.
DFH-02M: Inexplicable Subtraction of One
| Type | Severity | Location |
|---|---|---|
| Mathematical Operations | ![]() | DynamicFeeHook.sol:L246 |
Description:
The DynamicFeeHook::_updateVolEstimate function will reduce the impact of the previous twatTick by subtracting 1 from the window multiplier in a non-standard manner.
Impact:
The previous time-weighted average tick value is underestimated consistently due to a reduction of its multiplier.
Example:
237function _updateVolEstimate() internal {238 VolatilityData storage v = volatility;239 uint32 nowTs = uint32(block.timestamp);240 uint32 dt = nowTs - v.lastTimestamp;241 int24 tickNow = _getCurrentTick();242
243 if (dt >= v.twatWindow && v.twatWindow != 0) {244 // Simplified TWAT calculation using native types245 int32 window = int32(v.twatWindow);246 int32 weightedOld = int32(v.twatTick) * (window - 1);247 int32 newTwat = (weightedOld + int32(tickNow)) / window;248 249 v.twatTick = int24(newTwat);250 v.lastTimestamp = nowTs;251 v.lastTick = tickNow;252 }253}Recommendation:
We advise the code to instead reduce the window by a percentage rather than a fixed value if the algorithm should increase the impact of the current tick.
Alternatively, we advise the code to not reduce the multiplier of the old weight to ensure a fair and even evaluation of both the previous and the new tick in the system.
Alleviation (5dd8944ae71b382923f6971e9985eb7f427147ec):
The code was updated to utilize a percentage based EMA calculation as advised, ensuring that the old and new weights are factored into the overall calculation based on an alpha percentage.
DFH-03M: Incorrect Sigmoid Function Input
| Type | Severity | Location |
|---|---|---|
| Mathematical Operations | ![]() | DynamicFeeHook.sol:L282 |
Description:
The sigmoid function utilized in the DynamicFeeHook::_computeDynamicFee function will pass in the maximum value to yield as the sigmoidPart as c.maxFee even though the final result is clamped to that value after being added to the c.baseFee.
Impact:
The upper-bound Sigmoid function results will all result in the same c.maxFee evaluation even though the function itself is expected to be clamped to the relevant range.
Example:
266function _computeDynamicFee(uint256 vol)267 internal268 view269 returns (uint24 totalFeeBps)270{271 FeeConfig memory c = feeConfig;272 uint256 dynFeeBps;273
274 // ── Volatility thresholding ──275 if (vol < c.epsilon) {276 dynFeeBps = c.minFee;277 } else {278 uint256 sigmoidPart = _sigmoid(279 vol,280 c.k,281 c.midpoint,282 c.maxFee283 );284 dynFeeBps = c.baseFee + sigmoidPart;285
286 // clamp to bounds287 if (dynFeeBps < c.minFee) dynFeeBps = c.minFee;288 if (dynFeeBps > c.maxFee) dynFeeBps = c.maxFee;289 }290
291 totalFeeBps = uint24(dynFeeBps);292}Recommendation:
We advise the input argument to be updated to c.maxFee - c.baseFee, ensuring that the Sigmoid function properly flows through all permitted values of the dynamic fee.
Alleviation (5dd8944ae71b382923f6971e9985eb7f427147ec):
The input has been updated to the correct fee value, ensuring that the Sigmoid function calculations are performed correctly.
DFH-04M: Incorrect Value Clamping
| Type | Severity | Location |
|---|---|---|
| Mathematical Operations | ![]() | DynamicFeeHook.sol:L316 |
Description:
The upper bound result of the DynamicFeeHook::_expFixed function is clamped to the value of type(uint256).max which is insecure as the function's result is added to the ONE_18 literal, resulting in a Denial-of-Service overflow.
Impact:
Any significantly high exponent for the Sigmoid function would result in a Denial-of-Service due to an arithmetic overflow of all AMM pairs that rely on it.
Example:
294/* ── Sigmoid helper: returns 0 … maxY (bps) ──295 * y = maxY / (1 + e^(-k(x-mid)))296 * k, x, mid are interpreted in same 'vol' units (VOL_SCALE).297 */298function _sigmoid(299 uint256 x,300 uint256 k,301 uint256 mid,302 uint256 maxY303) internal pure returns (uint256 y) {304 // convert to signed 64.64 fixed-point for expo305 int256 exponent = -int256(k) * (int256(x) - int256(mid)); // units: VOL_SCALE306 exponent = exponent * int256(ONE_18) / int256(VOL_SCALE); // scale to 1e18 fp307
308 uint256 expRes = _expFixed(exponent); // 1e18 fixed309 uint256 denom = ONE_18 + expRes; // 1e18 fixed310 y = (maxY * ONE_18) / denom; // → bps311}312
313/* ── 1e18-fp exponential via 6-term Maclaurin (± ~1e-6 rel error for small |x|) ── */314function _expFixed(int256 x) internal pure returns (uint256) {315 // Handle extremes crudely to avoid overflow in series316 if (x > 60e18) return type(uint256).max; // e^60 ~ 1e26317 if (x < -60e18) return 0;318
319 int256 term = int256(ONE_18); // 1.0320 int256 sum = term;321
322 for (uint256 i = 1; i < 6; ++i) {323 term = (term * x) / int256(int256(i) * int256(ONE_18));324 sum += term;325 }326
327 return uint256(sum);328}Recommendation:
We advise the upper bound value to be clamped to type(uint256).max - ONE_18, and all outputs of the DynamicFeeHook::_expFixed function to be clamped to the same upper limit to ensure no Denial-of-Service of the DynamicFeeHook::_expFixed function can occur and thus no DoS of the before swap hooks of all AMMs relying on the DynamicFeeHook contract.
Alleviation (5dd8944ae71b382923f6971e9985eb7f427147ec):
The code was updated to properly clamp exponential evaluations to a value that would not result in an overflow, addressing this exhibit.
DFH-05M: Inexistent Protection of Re-Initialization
| Type | Severity | Location |
|---|---|---|
| Logical Fault | ![]() | DynamicFeeHook.sol:L78-L91 |
Description:
The DynamicFeeHook::initialize function does not prohibit re-initializations, permitting any user to reset the volatility trackers in the contract.
Impact:
The TWAT mechanism of the DynamicFeeHook can be reset via initialization at any point by any party rendering it insecure.
Example:
78function initialize(79 PoolKey memory _designatedPoolKey,80 uint32 twatWindowSeconds // e.g. 6081) external {82 (uint160 sqrtPriceX96, int24 initTick, , ) = poolManager.getSlot0(_designatedPoolKey.toId());83 designatedPoolKey = _designatedPoolKey;84 require(sqrtPriceX96 > 0, "Pool not initialized");85 volatility.twatWindow = twatWindowSeconds;86
87 // seed TWAT with current tick88 volatility.twatTick = initTick;89 volatility.lastTick = initTick;90 volatility.lastTimestamp = uint32(block.timestamp);91}Recommendation:
We advise the function to protect against reinitializations by evaluating whether the volatility.lastTimestamp has been set to a non-zero value.
Alleviation (5dd8944ae71b382923f6971e9985eb7f427147ec):
The DynamicFeeHook::initialize function was updated to prevent re-initialization by evaluating whether the volatility.lastTimestamp is 0.



