From d26d59690c8dc99cbb773a7ac660c889c067a4d0 Mon Sep 17 00:00:00 2001 From: minhtr09 <22521523@gm.uit.edu.vn> Date: Fri, 19 Jun 2026 15:12:46 +0700 Subject: [PATCH 1/2] feat: add oracle price conditions (Chainlink + Pyth) to conditional swap hook --- remappings.txt | 3 +- src/hooks/swap/KSConditionalSwapHook.sol | 86 +- .../oracle/external/AggregatorV3Interface.sol | 17 + src/interfaces/oracle/external/IPyth.sol | 17 + src/libraries/OracleLib.sol | 176 ++++ test/ConditionalSwap.t.sol | 809 +++++++++++++----- test/mocks/MockChainlinkFeed.sol | 33 + test/mocks/MockPyth.sol | 43 + 8 files changed, 957 insertions(+), 227 deletions(-) create mode 100644 src/interfaces/oracle/external/AggregatorV3Interface.sol create mode 100644 src/interfaces/oracle/external/IPyth.sol create mode 100644 src/libraries/OracleLib.sol create mode 100644 test/mocks/MockChainlinkFeed.sol create mode 100644 test/mocks/MockPyth.sol diff --git a/remappings.txt b/remappings.txt index 18131d6..d3d211f 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,2 +1,3 @@ forge-std/=lib/ks-common-sc/lib/forge-std/src/ -ks-common-sc/=lib/ks-common-sc \ No newline at end of file +ks-common-sc/=lib/ks-common-sc +openzeppelin-contracts=lib/ks-common-sc/lib/openzeppelin-contracts \ No newline at end of file diff --git a/src/hooks/swap/KSConditionalSwapHook.sol b/src/hooks/swap/KSConditionalSwapHook.sol index add6024..f8c7246 100644 --- a/src/hooks/swap/KSConditionalSwapHook.sol +++ b/src/hooks/swap/KSConditionalSwapHook.sol @@ -2,14 +2,16 @@ pragma solidity ^0.8.0; import {IKSSmartIntentHook} from '../../interfaces/hooks/IKSSmartIntentHook.sol'; -import {BaseStatefulHook} from '../base/BaseStatefulHook.sol'; -import {TokenHelper} from 'ks-common-sc/src/libraries/token/TokenHelper.sol'; - +import {OracleLib} from '../../libraries/OracleLib.sol'; import {ActionData} from '../../types/ActionData.sol'; import {IntentData} from '../../types/IntentData.sol'; +import {BaseStatefulHook} from '../base/BaseStatefulHook.sol'; +import {CalldataDecoder} from 'ks-common-sc/src/libraries/calldata/CalldataDecoder.sol'; +import {TokenHelper} from 'ks-common-sc/src/libraries/token/TokenHelper.sol'; contract KSConditionalSwapHook is BaseStatefulHook { using TokenHelper for address; + using CalldataDecoder for bytes; error InvalidTokenIn(address tokenIn, address actualTokenIn); error AmountInMismatch(uint256 amountIn, uint256 actualAmountIn); @@ -38,7 +40,9 @@ contract KSConditionalSwapHook is BaseStatefulHook { * @param timeLimits The limits of the swap time (minTime 128bits, maxTime 128bits) * @param amountInLimits The limits of the swap amount (minAmountIn 128bits, maxAmountIn 128bits) * @param maxFees The max fees (srcFee 128bits, dstFee 128bits) - * @param priceLimits The limits of price (tokenOut/tokenIn denominated by 1e18) (minPrice 128bits, maxPrice 128bits) + * @param priceLimits The limits of the realized price (tokenOut/tokenIn denominated by 1e18) (minPrice 128bits, maxPrice 128bits) + * @param oracle The per-token oracle config (each leg Chainlink or Pyth), carrying the + * per-token market-price bands and the staleness/slippage params */ struct SwapCondition { uint8 swapLimit; @@ -46,10 +50,10 @@ contract KSConditionalSwapHook is BaseStatefulHook { uint256 amountInLimits; uint256 maxFees; uint256 priceLimits; + OracleLib.OracleConfig oracle; } struct SwapValidationData { - SwapCondition[] swapConditions; bytes32 intentHash; uint256 intentIndex; address tokenIn; @@ -75,6 +79,8 @@ contract KSConditionalSwapHook is BaseStatefulHook { constructor(address[] memory initialRouters) BaseStatefulHook(initialRouters) {} + receive() external payable {} + modifier checkTokenLengths(ActionData calldata actionData) override { require(actionData.erc20Ids.length == 1, InvalidTokenData()); require(actionData.erc721Ids.length == 0, InvalidTokenData()); @@ -94,8 +100,13 @@ contract KSConditionalSwapHook is BaseStatefulHook { returns (uint256[] memory fees, bytes memory beforeExecutionData) { SwapHookData calldata swapHookData = _decodeHookData(intentData.coreData.hookIntentData); - (uint256 index, uint256 intentSrcFee, uint256 intentDstFee) = - _decodeAndValidateHookActionData(actionData.hookActionData, swapHookData); + ( + uint256 index, + uint256 intentSrcFee, + uint256 intentDstFee, + uint256 oracleUpdateIndex, + bytes[] calldata updateData + ) = _decodeHookActionData(actionData.hookActionData); address tokenIn = intentData.tokenData.erc20Data[actionData.erc20Ids[0]].token; address tokenOut = swapHookData.dstTokens[index]; @@ -106,11 +117,14 @@ contract KSConditionalSwapHook is BaseStatefulHook { InvalidTokenIn(tokenIn, swapHookData.srcTokens[index]) ); + if (updateData.length != 0) { + OracleLib.refresh(swapHookData.swapConditions[index][oracleUpdateIndex].oracle, updateData); + } + fees = new uint256[](1); fees[0] = (amountIn * intentSrcFee) / PRECISION; beforeExecutionData = abi.encode( SwapValidationData({ - swapConditions: swapHookData.swapConditions[index], intentHash: intentHash, intentIndex: index, tokenIn: tokenIn, @@ -161,13 +175,17 @@ contract KSConditionalSwapHook is BaseStatefulHook { uint256 price = (amountOut * DENOMINATOR) / amountIn; + SwapHookData calldata swapHookData = _decodeHookData(intentData.coreData.hookIntentData); + _validateSwapCondition( - validationData.swapConditions, + swapHookData.swapConditions[validationData.intentIndex], swapRecord[validationData.intentHash][validationData.intentIndex], price, amountIn, validationData.srcFeePercent, - validationData.dstFeePercent + validationData.dstFeePercent, + tokenIn, + tokenOut ); if (validationData.dstFeePercent == 0) { @@ -212,7 +230,9 @@ contract KSConditionalSwapHook is BaseStatefulHook { uint256 price, uint256 amountIn, uint256 srcFeePercent, - uint256 dstFeePercent + uint256 dstFeePercent, + address tokenIn, + address tokenOut ) internal { for (uint256 i; i < swapCondition.length; ++i) { SwapCondition calldata condition = swapCondition[i]; @@ -238,6 +258,10 @@ contract KSConditionalSwapHook is BaseStatefulHook { continue; } + if (!OracleLib.validate(condition.oracle, tokenIn, tokenOut, price)) { + continue; + } + if (!_increaseByOne(record, uint8(i), condition.swapLimit)) { continue; } @@ -287,17 +311,6 @@ contract KSConditionalSwapHook is BaseStatefulHook { return tokenOut.balanceOf(recipient); } - // @dev: equivalent to abi.decode(data, (SwapCondition)) - function _decodeSwapCondition(bytes calldata data) - internal - pure - returns (SwapCondition calldata swapCondition) - { - assembly ('memory-safe') { - swapCondition := data.offset - } - } - // @dev: equivalent to abi.decode(data, (SwapHookData)) function _decodeHookData(bytes calldata data) internal @@ -309,20 +322,29 @@ contract KSConditionalSwapHook is BaseStatefulHook { } } - // @dev: equivalent to abi.decode(data, (uint256, uint256, uint256, uint256)) - function _decodeAndValidateHookActionData(bytes calldata data, SwapHookData calldata swapHookData) + // @dev: equivalent to abi.encode(index, packedFees, oracleUpdateIndex, bytes[] updateData). + function _decodeHookActionData(bytes calldata data) internal - view - returns (uint256 index, uint256 intentSrcFee, uint256 intentDstFee) + pure + returns ( + uint256 index, + uint256 intentSrcFee, + uint256 intentDstFee, + uint256 oracleUpdateIndex, + bytes[] calldata updateData + ) { - uint256 packedFees; + index = data.decodeUint256(0); + intentSrcFee = data.decodeUint256(1); + oracleUpdateIndex = data.decodeUint256(2); + (uint256 length, uint256 offset) = data.decodeLengthOffset(3); assembly ('memory-safe') { - index := calldataload(data.offset) - packedFees := calldataload(add(data.offset, 0x20)) + updateData.length := length + updateData.offset := offset } - intentSrcFee = packedFees >> 128; - intentDstFee = uint128(packedFees); + intentDstFee = uint128(intentSrcFee); + intentSrcFee = intentSrcFee >> 128; } // @dev: equivalent to abi.decode(data, (SwapValidationData)) @@ -332,7 +354,7 @@ contract KSConditionalSwapHook is BaseStatefulHook { returns (SwapValidationData calldata validationData) { assembly ('memory-safe') { - validationData := add(data.offset, calldataload(data.offset)) + validationData := data.offset } } } diff --git a/src/interfaces/oracle/external/AggregatorV3Interface.sol b/src/interfaces/oracle/external/AggregatorV3Interface.sol new file mode 100644 index 0000000..2e34c73 --- /dev/null +++ b/src/interfaces/oracle/external/AggregatorV3Interface.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface AggregatorV3Interface { + function decimals() external view returns (uint8); + + function latestRoundData() + external + view + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ); +} diff --git a/src/interfaces/oracle/external/IPyth.sol b/src/interfaces/oracle/external/IPyth.sol new file mode 100644 index 0000000..cbb8337 --- /dev/null +++ b/src/interfaces/oracle/external/IPyth.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +interface IPyth { + struct Price { + int64 price; + uint64 conf; + int32 expo; + uint256 publishTime; + } + + function getUpdateFee(bytes[] calldata updateData) external view returns (uint256 feeAmount); + + function updatePriceFeeds(bytes[] calldata updateData) external payable; + + function getPriceNoOlderThan(bytes32 id, uint256 age) external view returns (Price memory price); +} diff --git a/src/libraries/OracleLib.sol b/src/libraries/OracleLib.sol new file mode 100644 index 0000000..d259374 --- /dev/null +++ b/src/libraries/OracleLib.sol @@ -0,0 +1,176 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.0; + +import {IERC20Metadata} from 'openzeppelin-contracts/contracts/interfaces/IERC20Metadata.sol'; +import {Math} from 'openzeppelin-contracts/contracts/utils/math/Math.sol'; + +import {AggregatorV3Interface} from '../interfaces/oracle/external/AggregatorV3Interface.sol'; +import {IPyth} from '../interfaces/oracle/external/IPyth.sol'; + +library OracleLib { + uint256 internal constant PRICE_PRECISION = 1e18; + uint256 internal constant BPS_DENOMINATOR = 10_000; + + enum OracleType { + NONE, + CHAINLINK, + PYTH + } + + /** + * @notice Oracle for a single token. + * @param oracleType type of the oracle. + * @param source Chainlink: token/USD feed; Pyth: IPyth contract. Zero = no oracle for this leg. + * @param priceId Pyth price id (unused for Chainlink). + * @param priceLimits USD price band, USD-per-whole-token 1e18 (min 128bits | max 128bits). + */ + struct TokenOracle { + OracleType oracleType; + address source; + bytes32 priceId; + uint256 priceLimits; + } + + /** + * @param oracleIn Input-token oracle. + * @param oracleOut Output-token oracle. + * @param oracleParams maxStaleness 128bits | maxDeviationBps 128bits (0 disables slippage guard). + */ + struct OracleConfig { + TokenOracle oracleIn; + TokenOracle oracleOut; + uint256 oracleParams; + } + + error InvalidOraclePrice(); + error StaleOraclePrice(); + + /** + * @notice Pushes the Pyth update + */ + function refresh(OracleConfig memory config, bytes[] calldata updateData) internal { + if (updateData.length == 0) return; + address pythIn = + config.oracleIn.oracleType == OracleType.PYTH ? config.oracleIn.source : address(0); + address pythOut = + config.oracleOut.oracleType == OracleType.PYTH ? config.oracleOut.source : address(0); + if (pythIn != address(0)) { + _pythUpdate(pythIn, updateData); + } + if (pythOut != address(0) && pythOut != pythIn) { + _pythUpdate(pythOut, updateData); + } + } + + /** + * @notice Validates a swap. Each configured leg's oracle price must sit in its band; an + * unconfigured leg is skipped. The slippage guard (realized within `maxDeviationBps` of + * the oracle ratio) applies only when both legs are set. True if no leg is set. + * @param realizedPrice Realized swap price, raw basis (`amountOut_raw * 1e18 / amountIn_raw`). + */ + function validate( + OracleConfig memory config, + address tokenIn, + address tokenOut, + uint256 realizedPrice + ) internal view returns (bool) { + bool hasIn = config.oracleIn.source != address(0); + bool hasOut = config.oracleOut.source != address(0); + if (!hasIn && !hasOut) return true; // no oracle configured + + uint256 maxStaleness = config.oracleParams >> 128; + uint256 priceIn; + uint256 priceOut; + + // market-price trigger: only configured legs are read and bounded + if (hasIn) { + priceIn = _usdPrice(config.oracleIn, maxStaleness); + if ( + priceIn < config.oracleIn.priceLimits >> 128 + || priceIn > uint128(config.oracleIn.priceLimits) + ) { + return false; + } + } + if (hasOut) { + priceOut = _usdPrice(config.oracleOut, maxStaleness); + if ( + priceOut < config.oracleOut.priceLimits >> 128 + || priceOut > uint128(config.oracleOut.priceLimits) + ) { + return false; + } + } + + // slippage guard: needs both legs to derive the ratio + uint256 maxDeviationBps = uint128(config.oracleParams); + if (maxDeviationBps != 0 && hasIn && hasOut) { + uint256 ratio = _ratio(priceIn, priceOut, tokenIn, tokenOut); + uint256 diff = realizedPrice > ratio ? realizedPrice - ratio : ratio - realizedPrice; + if (diff * BPS_DENOMINATOR > maxDeviationBps * ratio) return false; + } + + return true; + } + + /// @notice Both tokens' USD prices (1e18) and the derived raw-basis ratio. Both legs required. + function getPrices(OracleConfig memory config, address tokenIn, address tokenOut) + internal + view + returns (uint256 priceIn, uint256 priceOut, uint256 ratio) + { + uint256 maxStaleness = config.oracleParams >> 128; + priceIn = _usdPrice(config.oracleIn, maxStaleness); + priceOut = _usdPrice(config.oracleOut, maxStaleness); + ratio = _ratio(priceIn, priceOut, tokenIn, tokenOut); + } + + /// @dev ratio = (priceIn / priceOut) * 1e18 * 10^(decimalsOut - decimalsIn). + function _ratio(uint256 priceIn, uint256 priceOut, address tokenIn, address tokenOut) + private + view + returns (uint256) + { + uint8 decimalsIn = IERC20Metadata(tokenIn).decimals(); + uint8 decimalsOut = IERC20Metadata(tokenOut).decimals(); + if (decimalsOut >= decimalsIn) { + return + Math.mulDiv(priceIn, PRICE_PRECISION * (10 ** uint256(decimalsOut - decimalsIn)), priceOut); + } + return + Math.mulDiv(priceIn, PRICE_PRECISION, priceOut * (10 ** uint256(decimalsIn - decimalsOut))); + } + + function _pythUpdate(address pyth, bytes[] calldata updateData) private { + IPyth(pyth).updatePriceFeeds{value: IPyth(pyth).getUpdateFee(updateData)}(updateData); + } + + /// @dev Token oracle price, USD-per-whole-token (1e18). + function _usdPrice(TokenOracle memory oracle, uint256 maxStaleness) + private + view + returns (uint256) + { + if (oracle.oracleType == OracleType.CHAINLINK) { + (, int256 answer,, uint256 updatedAt,) = + AggregatorV3Interface(oracle.source).latestRoundData(); + if (answer <= 0) revert InvalidOraclePrice(); + if (maxStaleness != 0 && block.timestamp - updatedAt > maxStaleness) { + revert StaleOraclePrice(); + } + uint8 feedDecimals = AggregatorV3Interface(oracle.source).decimals(); + return Math.mulDiv(uint256(answer), PRICE_PRECISION, 10 ** feedDecimals); + } + + // PYTH: price = pyth.price * 10^(expo + 18) + uint256 age = maxStaleness == 0 ? type(uint256).max : maxStaleness; + IPyth.Price memory price = IPyth(oracle.source).getPriceNoOlderThan(oracle.priceId, age); + if (price.price <= 0) revert InvalidOraclePrice(); + + int256 exponent = int256(price.expo) + 18; + if (exponent >= 0) { + return uint256(uint64(price.price)) * (10 ** uint256(exponent)); + } + return uint256(uint64(price.price)) / (10 ** uint256(-exponent)); + } +} diff --git a/test/ConditionalSwap.t.sol b/test/ConditionalSwap.t.sol index ff31371..7e7630a 100644 --- a/test/ConditionalSwap.t.sol +++ b/test/ConditionalSwap.t.sol @@ -5,6 +5,11 @@ import './Base.t.sol'; import 'src/hooks/swap/KSConditionalSwapHook.sol'; +import {OracleLib} from 'src/libraries/OracleLib.sol'; + +import {MockChainlinkFeed} from './mocks/MockChainlinkFeed.sol'; +import {MockPyth} from './mocks/MockPyth.sol'; + contract ConditionalSwapTest is BaseTest { using SafeERC20 for IERC20; using TokenHelper for address; @@ -15,6 +20,9 @@ contract ConditionalSwapTest is BaseTest { bytes swapdata2 = hex'00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000f4a1d7fdf4890be35e71f3e0bbc4a0ec377eca3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000007a000000000000000000000000000000000000000000000000000000000000009e000000000000000000000000000000000000000000000000000000000000006e0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec70000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c5990000000000000000000000003674eD9c52D903C6c3A468592Ac27Fe71B3CD8490000000000000000000000000000000000000000000000000000000067db987b00000000000000000000000000000000000000000000000000000000000006800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000040f59b1df7000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000002000000000000000000000000066a9893cc07d91d95644aedd05d03f95e1dba8af000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000000000000022d473030f116ddee9f6b43ac78ba3000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000000000002300000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040a9d4c672000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000180000000000000000000000000655edce464cc797526600a462a8154650eee4b77000000000000000000000000000000000000000000000000000000003b9d5f1a000000000000000000000000000000000000000000000000000000003b9d5f1a00000000000000000000000000000000000000000000000006dac07944b594800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000f4a1d7fdf4890be35e71f3e0bbc4a0ec377eca3000000000000005fa94793ea0000001a371930340fc8fbcc09c409c467db9414000000000000000000000000000000000000000000000000000000000000001bdcffd1bf68c2c17dcf00a25c935efba96aa63b7f75dd43d42b3df2cf7273c2260fb4b38a9db829fbfdabcc6262ac3982f1d31366bfde12a7b67f6f31ba52b2cb0000000000000000000000000000000000000000000000000000000000000040d90ce4910000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000001000000000000000000000000007f86bf177dd4f3494b841a37e810a34dd56c829b000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c5990000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000006da929a6bb58cc0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000010000000000000000000000000011cbb0000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec70000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c599000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002000000000000000000000000003674eD9c52D903C6c3A468592Ac27Fe71B3CD849000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000000000000000000000000000000000000011c7210000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000f4a1d7fdf4890be35e71f3e0bbc4a0ec377eca30000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024f7b22536f75726365223a22222c22416d6f756e74496e555344223a22313030302e31373135393231313738353037222c22416d6f756e744f7574555344223a22313030302e34373538333032323939323331222c22526566657272616c223a22222c22466c616773223a302c22416d6f756e744f7574223a2231313636323536222c2254696d657374616d70223a313734323434333436392c22526f7574654944223a2263383438663432632d326465322d343364382d623366372d636637366362666430363536222c22496e74656772697479496e666f223a7b224b65794944223a2231222c225369676e6174757265223a224e39426b4975436430714961362f4d64736635717a61657863436c3754413539426e4d70454741437a74432b5875325176494a36444c34476b7075746b636f627554395657357a42744e427a5463736b4e7768434662372f6f52675173676970424e693878716d323869524b3048496834527a70316457512f437737676a58375168653270313853506966492b7550674e5a34647a5a6a4461686b664d416852796d7765783233714942536a65565a6f44483932596a534b4e546176396f2f2f634754766476336a52555538536841763153464b55514b54515470682f4d4f71534f7370646c37306632714155705274566d7739434b4d383347726164506b55546f5854684a2f6c734e784561634267395a37617a363837394d366d31517538465a687237796374367a4242524a774171464e6646436a364b523969307a4e702f665a2b6876394b6970455341666d5078634e4d67773d3d227d7d0000000000000000000000000000000000'; + bytes pythUpdateData = + hex'504e41550100000003b801000000040d00276f5b9bbd57764e3c885b2a93e2ec99e4410dddea133e2bcf6e6d2aa52220782156d586e84d0e4cd3174b4bd6f1451953c7d876cf5064c87f55ce2a661e8fee0102d23f5a5f808768e508584832f35dc3596d32630b52a05ff2f71e33d4d5333aa5672b33b488bb34d369258b1a49145244773a70e60d30306251b1634d2324bb2d01031056f9f2942476f131cbedc5b44d5582fd32b83c0ba9383b478e80c959e31af01fa88f50a33aeb7ab729d69a07d83fcba54f8f554e9c4060859ab762d3bff8f30004358c5fafc18f869fe8c1db5e2ebe2bdc8e501b8ad6c295740dd95eb878afd31358608385a66fdeeadc4c432560b51dfd2e1f57724e74bb3dd84197f0851068dc0106d9cc8e4f18b56badaf8e2f09ba38bfeadf5629780ff6ea3a396b4a3d631a1fce045552446529688efdc9c70d433faf55fbd10cf4a10d6aaa41d789eec29d30d90008877c385d9bfc1c69dcb82274cd076fc58086fb1e5b22eb50e5e11881e575125646de23f3656202b81402b3ab748b868fdce8abb5770a4e59c0a47e8794b2406c010a0f95e7560379535f9915806a8030dd90d732efdc9149a693ca611081b4ad01956cae625662a018eaa379bebcc75a58abff2334d8d21f736cba254c589ced4f5d010b48b5be572a2cadfc1fd0f74c23d7a8b43629e7c3600d1caa122d3932a370a9917a7ae16986001b931ad19a7167fc060ba3c328197d764f5a0f40f2c4c659c84e010cbd3cb104da104d83fb030623b292c046e8c7ab876866e9bf9444c31aa275f09b362e0cd2caf5d63fc67a4b4582351c00048258fc222fdcafd3a4f5cd973becdd000d6eb0c29f38df7fec3c52307caccf275999aded023f9d79048b17f98e184b6ac02dcdbd93dd6d146b59324d12f486336b110a2f814ff66c1ff299ff1427892b45000e5234724fad66ff9688510b538ad695dc8bcdce669b38f72fd6e0a276f173cc68571930fb308a047df9bb7f06a557b563d58484ca9d9e2bcecc387df20e3dd4d3010fd87f033e76d841cdf14bd2624008ef4f511bde4428590ba9f9fa782073afd96d744881c9bf908d6475b47d24dede2c5ab06c91c7b4df3b7a4b5c9818ec43e4f90010133189f049dcd388beb6a4063c9eeea9a2a54476efb79c3d39628e356673b278602849af9b6a0b9adbab8e72f533a5e9646441f813d8c8c37e985b5c064ad81a0067db904300000000001ae101faedac5851e32b9b23b5f9411a8c2bac4aae3ed4dd7b811dd1a72ea4aa7100000000072d7058014155575600000000000c3a7d8c00002710a12a5869dbbb38a8795bc72942ca40da9e0a5bd8020055002b89b9dc8fdf9f34709a5b106b472f0f39bb6ca9ce04b0fd7f2e971688e2e53b0000000005f6114f000000000000cda2fffffff80000000067db90430000000067db90420000000005f62653000000000000c5fa0cf52e2fe61280a39fe1b0710570a8df3293aaa7de950fbd77cb5a9fbd59efa8dae4ab333f4623a6936389052ab8b87098037e3541b7f5129257ad431e1806877866609c1fe71baae65c5c37c57825175aebe80f309cd48099b0040d6fb38f2dabac57e0717fb9b2ab916b0a94b4e931461438894d93a442dc6ccf8fb68da2a537171a52cfde7dea9243ee43193dcd7446429a85eb799e62d4fd72c56961aa0e0e33ce0488bd5938e05e1b4827d468d1732ba3e438bd99e2f0078e28842b59ee0f82f3cdf61b5ba8551bed77011a454079dba4ec47ddd6fd94450dc2c0928a53ac035b7de3792b6fe71bbd489f67c1e6aa005500e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43000007d16accffee00000000b1996851fffffff80000000067db90430000000067db9042000007d28b80ca4000000000bb14fbe80c73f40b6db4e39c00925c7d4c84e1da7a8f37bd95eb84812d210638850ea96d6b329964e81f75212ebdf8e7c5675b3449ba832014978ffa3f1aed4deb93c5355b06ab4a3cacc8e34f7f6e8e80e7a10de0f35cf6decc07c0b0409c8df3c3c90825d03d32e967b339885cf7dccc198e304ad02c5aa511e41519b898676021f3d4b43202ac5c01dc85a1fa3eecc7b649a1ca1a59b6e052cfd234141ab494bf1316b43c9bde3d074cea1b35d3726f16e6e3d8189b8aedc64c1fce8b2ad65ec2d36e0332101da6b69b19325a8d91369a0974404bce4596d217d9008dbe3217928a53ac035b7de3792b6fe71bbd489f67c1e6aa'; + uint256 feeBefore; uint256 feeAfter; uint256 maxSrcFee; @@ -25,7 +33,32 @@ contract ConditionalSwapTest is BaseTest { KSConditionalSwapHook conditionalSwapHook; uint256 currentPrice = 11_662_550_000_000; // USDC/BTC denominated by 1e18 - function setUp() public override { + // tokenIn = USDT (6 decimals), tokenOut = WBTC (8 decimals) on the mainnet fork. + // Per-token USD prices, USD-per-whole-token scaled by 1e18: + uint256 internal constant USDT_USD = 1e18; // $1 + uint256 internal constant BTC_USD = 100_000e18; // $100k + // Derived swap ratio (amountOut_raw * 1e18 / amountIn_raw) for the mock prices: 1e15. + uint256 internal constant ORACLE_RATIO = 1e15; + uint256 internal constant PYTH_FEE = 0.001 ether; + + bytes32 internal constant USDT_ID = keccak256('USDT/USD'); + bytes32 internal constant WBTC_ID = keccak256('WBTC/USD'); + + // --- Real mainnet oracle infrastructure (fork tests) --- + address internal constant CHAINLINK_USDT_USD = 0x3E7d1eAB13ad0104d2750B8863b489D65364e32D; + // WBTC tracks BTC; use the canonical BTC/USD aggregator for the WBTC leg. + address internal constant CHAINLINK_WBTC_USD = 0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c; + address internal constant PYTH_MAINNET = 0x4305FB66699C3B2702D4d05CF36551390A4c69C6; + bytes32 internal constant PYTH_USDT_USD = + 0x2b89b9dc8fdf9f34709a5b106b472f0f39bb6ca9ce04b0fd7f2e971688e2e53b; + bytes32 internal constant PYTH_BTC_USD = + 0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43; + + MockChainlinkFeed internal feedIn; + MockChainlinkFeed internal feedOut; + MockPyth internal pyth; + + function setUp() public virtual override { super.setUp(); address[] memory routers = new address[](1); @@ -34,8 +67,69 @@ contract ConditionalSwapTest is BaseTest { deal(tokenIn, mainAddress, 1e30); conditionalSwapHook = new KSConditionalSwapHook(routers); + // fund the hook so it can pay Pyth update fees out of its own balance + vm.deal(address(conditionalSwapHook), 1 ether); + + // mock oracles + feedIn = new MockChainlinkFeed(8, 1e8); // USDT/USD = $1 + feedOut = new MockChainlinkFeed(8, int256(100_000e8)); // WBTC/USD = $100k + pyth = new MockPyth(PYTH_FEE); + } + + function _emptyLeg() internal pure returns (OracleLib.TokenOracle memory) { + return OracleLib.TokenOracle(OracleLib.OracleType.NONE, address(0), bytes32(0), 0); } + function _chainlinkLeg(address feed, uint256 priceLimits) + internal + pure + returns (OracleLib.TokenOracle memory) + { + return OracleLib.TokenOracle(OracleLib.OracleType.CHAINLINK, feed, bytes32(0), priceLimits); + } + + function _pythLeg(address pyth_, bytes32 priceId, uint256 priceLimits) + internal + pure + returns (OracleLib.TokenOracle memory) + { + return OracleLib.TokenOracle(OracleLib.OracleType.PYTH, pyth_, priceId, priceLimits); + } + + function _config( + OracleLib.TokenOracle memory oracleIn, + OracleLib.TokenOracle memory oracleOut, + uint256 oracleParams + ) internal pure returns (OracleLib.OracleConfig memory) { + return OracleLib.OracleConfig(oracleIn, oracleOut, oracleParams); + } + + function _noOracle() internal pure returns (OracleLib.OracleConfig memory) { + return _config(_emptyLeg(), _emptyLeg(), 0); + } + + /// @dev oracleParams packed: maxStaleness 128bits | maxDeviationBps 128bits. + function _params(uint256 maxStaleness, uint256 maxDeviationBps) internal pure returns (uint256) { + return (maxStaleness << 128) | maxDeviationBps; + } + + /// @dev USD price band [price*(1-bpsBelow), price*(1+bpsAbove)], packed min 128 | max 128. + function _band(uint256 price, uint256 bpsBelow, uint256 bpsAbove) + internal + pure + returns (uint256) + { + uint256 lower = (price * (10_000 - bpsBelow)) / 10_000; + uint256 upper = (price * (10_000 + bpsAbove)) / 10_000; + return (lower << 128) | upper; + } + + uint256 internal constant FULL_BAND = (uint256(0) << 128) | type(uint128).max; + + // --------------------------------------------------------------------------- + // Non-oracle conditional swap tests + // --------------------------------------------------------------------------- + struct TestFuzz_ConditionalSwap_Params { uint256 mode; uint256 maxFeeBefore; @@ -81,17 +175,6 @@ contract ConditionalSwapTest is BaseTest { (address caller, bytes memory dkSignature, bytes memory gdSignature) = _getCallerAndSignatures(params.mode, intentData, actionData); - if (feeBefore > maxSrcFee || feeAfter > maxDstFee) { - vm.expectRevert( - abi.encodeWithSelector( - KSConditionalSwapHook.InvalidSwap.selector, feeBefore, feeAfter, maxSrcFee, maxDstFee - ) - ); - vm.startPrank(caller); - router.execute(intentData, dkSignature, guardian, gdSignature, actionData); - return; - } - uint256[2] memory routerBefore = [tokenIn.balanceOf(address(router)), tokenOut.balanceOf(address(router))]; uint256[2] memory mainAddressBefore = @@ -134,30 +217,12 @@ contract ConditionalSwapTest is BaseTest { KSConditionalSwapHook.SwapCondition[] memory condition = new KSConditionalSwapHook.SwapCondition[](3); - - { - condition[0] = KSConditionalSwapHook.SwapCondition({ - swapLimit: 1, - timeLimits: ((vm.getBlockTimestamp() - 100) << 128) | (vm.getBlockTimestamp() + 100), - amountInLimits: (swapAmount << 128) | swapAmount, - maxFees: (0 << 128) | type(uint128).max, - priceLimits: (0 << 128) | type(uint128).max - }); - condition[1] = KSConditionalSwapHook.SwapCondition({ - swapLimit: 1, - timeLimits: ((vm.getBlockTimestamp() + 500) << 128) | (vm.getBlockTimestamp() + 700), - amountInLimits: (swapAmount << 128) | swapAmount, - maxFees: (0 << 128) | type(uint128).max, - priceLimits: (0 << 128) | type(uint128).max - }); - condition[2] = KSConditionalSwapHook.SwapCondition({ - swapLimit: 1, - timeLimits: ((vm.getBlockTimestamp() + 1000) << 128) | (vm.getBlockTimestamp() + 1200), - amountInLimits: (swapAmount << 128) | swapAmount, - maxFees: (0 << 128) | type(uint128).max, - priceLimits: (0 << 128) | type(uint128).max - }); - } + condition[0] = + _timeCondition(((vm.getBlockTimestamp() - 100) << 128) | (vm.getBlockTimestamp() + 100)); + condition[1] = + _timeCondition(((vm.getBlockTimestamp() + 500) << 128) | (vm.getBlockTimestamp() + 700)); + condition[2] = + _timeCondition(((vm.getBlockTimestamp() + 1000) << 128) | (vm.getBlockTimestamp() + 1200)); IntentData memory intentData; { @@ -168,59 +233,31 @@ contract ConditionalSwapTest is BaseTest { swapAmount = tmpSwapAmount; } - ActionData memory actionData; - { - TokenData memory tokenData; - tokenData.erc20Data = new ERC20Data[](1); - tokenData.erc20Data[0] = ERC20Data({token: tokenIn, amount: swapAmount, permitData: ''}); - actionData = _getActionData( - tokenData, - abi.encode( - tokenIn, - tokenOut, - swapAmount, - 1000, - feeAfter == 0 ? mainAddress : address(router), - mainAddress - ), - true - ); - } + ActionData memory actionData = _mockSwapAction(); - // swap 1 - { - _swap(mode, intentData, actionData, 0, 0); - } + _swap(mode, intentData, actionData, 0, 0); - // swap 2 - { - vm.warp(vm.getBlockTimestamp() + 500); - actionData.nonce += 1; - _swap(mode, intentData, actionData, 0, 1); - } + vm.warp(vm.getBlockTimestamp() + 500); + actionData.nonce += 1; + _swap(mode, intentData, actionData, 0, 1); - // swap 3 - { - vm.warp(vm.getBlockTimestamp() + 600); - actionData.nonce += 1; - _swap(mode, intentData, actionData, 0, 2); - } + vm.warp(vm.getBlockTimestamp() + 600); + actionData.nonce += 1; + _swap(mode, intentData, actionData, 0, 2); } function test_DCASwap_PriceBased(uint256 mode) public { mode = bound(mode, 0, 2); KSConditionalSwapHook.SwapCondition[] memory condition = new KSConditionalSwapHook.SwapCondition[](1); - - { - condition[0] = KSConditionalSwapHook.SwapCondition({ - swapLimit: 4, - timeLimits: (0 << 128) | type(uint128).max, - amountInLimits: (swapAmount << 128) | swapAmount, - maxFees: (0 << 128) | type(uint128).max, - priceLimits: ((1_000_000_000_000 - 100) << 128) | (1_000_000_000_000 + 100) - }); - } + condition[0] = KSConditionalSwapHook.SwapCondition({ + swapLimit: 4, + timeLimits: (0 << 128) | type(uint128).max, + amountInLimits: (swapAmount << 128) | swapAmount, + maxFees: (0 << 128) | type(uint128).max, + priceLimits: ((1_000_000_000_000 - 100) << 128) | (1_000_000_000_000 + 100), + oracle: _noOracle() + }); IntentData memory intentData; { @@ -230,77 +267,21 @@ contract ConditionalSwapTest is BaseTest { _setUpMainAddress(intentData, false); swapAmount = tmpSwapAmount; } - ActionData memory actionData; - { - TokenData memory tokenData; - tokenData.erc20Data = new ERC20Data[](1); - tokenData.erc20Data[0] = ERC20Data({token: tokenIn, amount: swapAmount, permitData: ''}); - actionData = _getActionData( - tokenData, - abi.encode( - tokenIn, - tokenOut, - swapAmount, - 1000, - feeAfter == 0 ? mainAddress : address(router), - mainAddress - ), - true - ); - } - - // swap 1 - { - _swap(mode, intentData, actionData, 0, 0); - } - - // swap 2 - { - actionData.nonce += 1; - _swap(mode, intentData, actionData, 1, 0); - } - - // swap 3 - { - actionData.nonce += 1; - _swap(mode, intentData, actionData, 2, 0); - } - } - - function _swap( - uint256 mode, - IntentData memory intentData, - ActionData memory actionData, - uint256 swapCount, - uint256 index - ) internal { - (address caller, bytes memory dkSignature, bytes memory gdSignature) = - _getCallerAndSignatures(mode, intentData, actionData); - bytes32 hash = router.hashTypedIntentData(intentData); - - uint256 balanceBefore = tokenOut.balanceOf(mainAddress); - - assertEq(conditionalSwapHook.getSwapExecutionCount(hash, 0, index), swapCount); - vm.startPrank(caller); - router.execute(intentData, dkSignature, guardian, gdSignature, actionData); - vm.stopPrank(); - assertEq(conditionalSwapHook.getSwapExecutionCount(hash, 0, index), swapCount + 1); + ActionData memory actionData = _mockSwapAction(); - assertGt(tokenOut.balanceOf(mainAddress), balanceBefore); + _swap(mode, intentData, actionData, 0, 0); + actionData.nonce += 1; + _swap(mode, intentData, actionData, 1, 0); + actionData.nonce += 1; + _swap(mode, intentData, actionData, 2, 0); } function testRevert_InvalidTimeCondition(uint256 mode) public { mode = bound(mode, 0, 2); KSConditionalSwapHook.SwapCondition[] memory condition = new KSConditionalSwapHook.SwapCondition[](1); - - condition[0] = KSConditionalSwapHook.SwapCondition({ - swapLimit: 1, - timeLimits: ((vm.getBlockTimestamp() + 100) << 128) | (vm.getBlockTimestamp() + 1000), - amountInLimits: (0 << 128) | type(uint128).max, - maxFees: (0 << 128) | type(uint128).max, - priceLimits: (0 << 128) | type(uint128).max - }); + condition[0] = + _timeCondition(((vm.getBlockTimestamp() + 100) << 128) | (vm.getBlockTimestamp() + 1000)); IntentData memory intentData = _getIntentData(0, type(uint128).max, condition); _setUpMainAddress(intentData, false); @@ -321,17 +302,16 @@ contract ConditionalSwapTest is BaseTest { mode = bound(mode, 0, 2); KSConditionalSwapHook.SwapCondition[] memory condition = new KSConditionalSwapHook.SwapCondition[](1); - condition[0] = KSConditionalSwapHook.SwapCondition({ swapLimit: 1, timeLimits: ((vm.getBlockTimestamp() - 100) << 128) | (vm.getBlockTimestamp() + 100), amountInLimits: (0 << 128) | type(uint128).max, maxFees: (0 << 128) | type(uint128).max, - priceLimits: (uint256(type(uint128).max) << 128) | type(uint128).max + priceLimits: (uint256(type(uint128).max) << 128) | type(uint128).max, + oracle: _noOracle() }); IntentData memory intentData = _getIntentData(0, type(uint128).max, condition); - _setUpMainAddress(intentData, false); ActionData memory actionData = _getActionData( @@ -365,21 +345,18 @@ contract ConditionalSwapTest is BaseTest { bytes32 hash = router.hashTypedIntentData(intentData); assertEq(conditionalSwapHook.getSwapExecutionCount(hash, 0, 0), 0); - { - (address caller, bytes memory dkSignature, bytes memory gdSignature) = - _getCallerAndSignatures(mode, intentData, actionData); + (address caller, bytes memory dkSignature, bytes memory gdSignature) = + _getCallerAndSignatures(mode, intentData, actionData); - vm.startPrank(caller); - router.execute(intentData, dkSignature, guardian, gdSignature, actionData); - actionData.nonce += 1; - (caller, dkSignature, gdSignature) = _getCallerAndSignatures(mode, intentData, actionData); - vm.startPrank(caller); - vm.expectRevert(KSConditionalSwapHook.InvalidSwap.selector); - router.execute(intentData, dkSignature, guardian, gdSignature, actionData); - } - { - assertEq(conditionalSwapHook.getSwapExecutionCount(hash, 0, 0), 1); - } + vm.startPrank(caller); + router.execute(intentData, dkSignature, guardian, gdSignature, actionData); + actionData.nonce += 1; + (caller, dkSignature, gdSignature) = _getCallerAndSignatures(mode, intentData, actionData); + vm.startPrank(caller); + vm.expectRevert(KSConditionalSwapHook.InvalidSwap.selector); + router.execute(intentData, dkSignature, guardian, gdSignature, actionData); + + assertEq(conditionalSwapHook.getSwapExecutionCount(hash, 0, 0), 1); } function testRevert_InvalidTokenIn(uint256 mode) public { @@ -393,7 +370,6 @@ contract ConditionalSwapTest is BaseTest { ActionData memory actionData = _getActionData( intentData.tokenData, _adjustRecipient(feeAfter == 0 ? swapdata2 : swapdata), false ); - actionData.erc20Ids[0] = 0; (address caller, bytes memory dkSignature, bytes memory gdSignature) = @@ -452,6 +428,478 @@ contract ConditionalSwapTest is BaseTest { router.execute(intentData, dkSignature, guardian, gdSignature, actionData); } + function test_Chainlink_MarketTrigger_Pass(uint256 mode) public { + mode = bound(mode, 0, 2); + OracleLib.OracleConfig memory cfg = _config( + _chainlinkLeg(address(feedIn), _band(USDT_USD, 100, 100)), + _chainlinkLeg(address(feedOut), _band(BTC_USD, 100, 100)), + 0 + ); + _expectSwapOk(mode, cfg, _amountOutFor(ORACLE_RATIO), 0, new bytes[](0)); + } + + function test_Chainlink_MarketTrigger_Revert(uint256 mode) public { + mode = bound(mode, 0, 2); + // tokenOut band sits entirely above the live BTC price -> never met + OracleLib.OracleConfig memory cfg = _config( + _chainlinkLeg(address(feedIn), _band(USDT_USD, 100, 100)), + _chainlinkLeg(address(feedOut), (uint256(BTC_USD * 2) << 128) | type(uint128).max), + 0 + ); + _expectSwapRevert(mode, cfg, _amountOutFor(ORACLE_RATIO), 0, new bytes[](0)); + } + + function test_Chainlink_SlippageGuard_Pass(uint256 mode) public { + mode = bound(mode, 0, 2); + OracleLib.OracleConfig memory cfg = _config( + _chainlinkLeg(address(feedIn), FULL_BAND), + _chainlinkLeg(address(feedOut), FULL_BAND), + _params(0, 1000) // 10% tolerance + ); + _expectSwapOk(mode, cfg, _amountOutFor((ORACLE_RATIO * 105) / 100), 0, new bytes[](0)); // +5% + } + + function test_Chainlink_SlippageGuard_Revert(uint256 mode) public { + mode = bound(mode, 0, 2); + OracleLib.OracleConfig memory cfg = _config( + _chainlinkLeg(address(feedIn), FULL_BAND), + _chainlinkLeg(address(feedOut), FULL_BAND), + _params(0, 100) // 1% tolerance + ); + _expectSwapRevert(mode, cfg, _amountOutFor((ORACLE_RATIO * 105) / 100), 0, new bytes[](0)); // +5% + } + + function test_Chainlink_SingleLeg_Out(uint256 mode) public { + mode = bound(mode, 0, 2); + // only the output token is constrained; the input leg is left unconfigured + OracleLib.OracleConfig memory cfg = + _config(_emptyLeg(), _chainlinkLeg(address(feedOut), _band(BTC_USD, 100, 100)), 0); + _expectSwapOk(mode, cfg, _amountOutFor(ORACLE_RATIO), 0, new bytes[](0)); + } + + function test_Pyth_Update_And_Validate(uint256 mode) public { + mode = bound(mode, 0, 2); + OracleLib.OracleConfig memory cfg = _config( + _pythLeg(address(pyth), USDT_ID, _band(USDT_USD, 100, 100)), + _pythLeg(address(pyth), WBTC_ID, _band(BTC_USD, 100, 100)), + _params(3600, 0) + ); + + uint256 hookBalBefore = address(conditionalSwapHook).balance; + (IntentData memory intentData, ActionData memory actionData) = _buildIntentAndAction( + _single(cfg), _amountOutFor(ORACLE_RATIO), 0, _pythUpdateData(vm.getBlockTimestamp()) + ); + + uint256 balBefore = IERC20(tokenOut).balanceOf(mainAddress); + _executeSwap(mode, intentData, actionData); + + assertGt(IERC20(tokenOut).balanceOf(mainAddress), balBefore); + assertEq(pyth.updateCount(), 1); // hook pushed exactly one update + assertEq(address(pyth).balance, PYTH_FEE); // fee delivered to Pyth + assertEq(address(conditionalSwapHook).balance, hookBalBefore - PYTH_FEE); // paid by the hook + } + + function test_Pyth_StalePrice_Revert(uint256 mode) public { + mode = bound(mode, 0, 2); + OracleLib.OracleConfig memory cfg = _config( + _pythLeg(address(pyth), USDT_ID, FULL_BAND), + _pythLeg(address(pyth), WBTC_ID, FULL_BAND), + _params(100, 0) // 100s staleness bound + ); + // publish time well in the past -> getPriceNoOlderThan reverts during afterExecution + vm.warp(vm.getBlockTimestamp() + 1000); + bytes[] memory updateData = _pythUpdateData(vm.getBlockTimestamp() - 1000); + + (IntentData memory intentData, ActionData memory actionData) = + _buildIntentAndAction(_single(cfg), _amountOutFor(ORACLE_RATIO), 0, updateData); + + (address caller, bytes memory dk, bytes memory gd) = + _getCallerAndSignatures(mode, intentData, actionData); + vm.startPrank(caller); + vm.expectRevert(MockPyth.StalePrice.selector); + router.execute(intentData, dk, guardian, gd, actionData); + } + + /// @dev Two conditions, each with its own oracle (Chainlink then Pyth) in one intent index. + function test_PerCondition_IndependentOracles(uint256 mode) public { + mode = bound(mode, 0, 2); + + KSConditionalSwapHook.SwapCondition[] memory conditions = + new KSConditionalSwapHook.SwapCondition[](2); + // condition 0: Chainlink, tokenOut band above the live price -> never matches + conditions[0] = _oracleCondition( + _config( + _chainlinkLeg(address(feedIn), _band(USDT_USD, 100, 100)), + _chainlinkLeg(address(feedOut), (uint256(BTC_USD * 2) << 128) | type(uint128).max), + 0 + ) + ); + // condition 1: Pyth, bands bracket the live prices -> matches + conditions[1] = _oracleCondition( + _config( + _pythLeg(address(pyth), USDT_ID, _band(USDT_USD, 100, 100)), + _pythLeg(address(pyth), WBTC_ID, _band(BTC_USD, 100, 100)), + _params(3600, 0) + ) + ); + + // oracleUpdateIndex = 1 -> refresh uses condition 1's (Pyth) config + (IntentData memory intentData, ActionData memory actionData) = _buildIntentAndAction( + conditions, _amountOutFor(ORACLE_RATIO), 1, _pythUpdateData(vm.getBlockTimestamp()) + ); + + bytes32 hash = router.hashTypedIntentData(intentData); + uint256 balBefore = IERC20(tokenOut).balanceOf(mainAddress); + _executeSwap(mode, intentData, actionData); + + assertGt(IERC20(tokenOut).balanceOf(mainAddress), balBefore); + assertEq(conditionalSwapHook.getSwapExecutionCount(hash, 0, 0), 0); + assertEq(conditionalSwapHook.getSwapExecutionCount(hash, 0, 1), 1); + } + + function test_Fork_ChainlinkReal_MarketTrigger_Pass(uint256 mode) public { + mode = bound(mode, 0, 2); + (uint256 priceIn, uint256 priceOut, uint256 ratio) = + _readReal(_realChainlink(FULL_BAND, FULL_BAND)); + + OracleLib.OracleConfig memory cfg = + _realChainlink(_band(priceIn, 100, 100), _band(priceOut, 100, 100)); + _expectSwapOk(mode, cfg, _amountOutFor(ratio), 0, new bytes[](0)); + } + + function test_Fork_ChainlinkReal_MarketTrigger_Revert(uint256 mode) public { + mode = bound(mode, 0, 2); + (, uint256 priceOut, uint256 ratio) = _readReal(_realChainlink(FULL_BAND, FULL_BAND)); + + OracleLib.OracleConfig memory cfg = + _realChainlink(FULL_BAND, (uint256(priceOut * 2) << 128) | type(uint128).max); + _expectSwapRevert(mode, cfg, _amountOutFor(ratio), 0, new bytes[](0)); + } + + function test_Fork_ChainlinkReal_SlippageGuard_Revert(uint256 mode) public { + mode = bound(mode, 0, 2); + (,, uint256 ratio) = _readReal(_realChainlink(FULL_BAND, FULL_BAND)); + + OracleLib.OracleConfig memory cfg = _realChainlink(FULL_BAND, FULL_BAND); + cfg.oracleParams = _params(0, 200); // 2% tolerance + _expectSwapRevert(mode, cfg, _amountOutFor((ratio * 110) / 100), 0, new bytes[](0)); // +10% + } + + function test_Fork_PythReal_Read_Pass(uint256 mode) public { + mode = bound(mode, 0, 2); + (uint256 priceIn, uint256 priceOut, uint256 ratio) = _readReal(_realPyth(FULL_BAND, FULL_BAND)); + + OracleLib.OracleConfig memory cfg = + _realPyth(_band(priceIn, 200, 200), _band(priceOut, 200, 200)); + _expectSwapOk(mode, cfg, _amountOutFor(ratio), 0, new bytes[](0)); + } + + function test_Fork_PythReal_Update_Pass() public { + bytes[] memory updateData = new bytes[](1); + updateData[0] = pythUpdateData; + + (uint256 priceIn, uint256 priceOut, uint256 ratio) = _readReal(_realPyth(FULL_BAND, FULL_BAND)); + OracleLib.OracleConfig memory cfg = + _realPyth(_band(priceIn, 200, 200), _band(priceOut, 200, 200)); + + (IntentData memory intentData, ActionData memory actionData) = + _buildIntentAndAction(_single(cfg), _amountOutFor(ratio), 0, updateData); + + uint256 balBefore = IERC20(tokenOut).balanceOf(mainAddress); + _executeSwap(1, intentData, actionData); + assertGt(IERC20(tokenOut).balanceOf(mainAddress), balBefore); + } + + function test_Fork_RealSwap_ChainlinkChainlink_Pass(uint256 mode) public { + _runRealSwapOracle(mode, false, false, true); + } + + function test_Fork_RealSwap_ChainlinkChainlink_Fail(uint256 mode) public { + _runRealSwapOracle(mode, false, false, false); + } + + function test_Fork_RealSwap_PythPyth_Pass(uint256 mode) public { + _runRealSwapOracle(mode, true, true, true); + } + + function test_Fork_RealSwap_PythPyth_Fail(uint256 mode) public { + _runRealSwapOracle(mode, true, true, false); + } + + function test_Fork_RealSwap_ChainlinkPyth_Pass(uint256 mode) public { + _runRealSwapOracle(mode, false, true, true); + } + + function test_Fork_RealSwap_ChainlinkPyth_Fail(uint256 mode) public { + _runRealSwapOracle(mode, false, true, false); + } + + function test_Fork_RealSwap_PythChainlink_Pass(uint256 mode) public { + _runRealSwapOracle(mode, true, false, true); + } + + function test_Fork_RealSwap_PythChainlink_Fail(uint256 mode) public { + _runRealSwapOracle(mode, true, false, false); + } + + function _runRealSwapOracle(uint256 mode, bool inPyth, bool outPyth, bool ok) internal { + mode = bound(mode, 0, 2); + + // read the live per-leg USD prices (read-only; Pyth legs are already on-chain at the fork) + (uint256 priceIn, uint256 priceOut,) = + _readReal(_config(_legIn(inPyth, FULL_BAND), _legOut(outPyth, FULL_BAND), 0)); + + uint256 bandOut = + ok ? _band(priceOut, 100, 100) : (uint256(priceOut * 2) << 128) | type(uint128).max; + + OracleLib.OracleConfig memory cfg = _config( + _legIn(inPyth, _band(priceIn, 100, 100)), + _legOut(outPyth, bandOut), + _params(0, 1000) // 10% slippage tolerance (live price vs the captured route) + ); + + bytes[] memory updateData = new bytes[](0); + if (inPyth || outPyth) { + updateData = new bytes[](1); + updateData[0] = pythUpdateData; + } + + KSConditionalSwapHook.SwapCondition[] memory condition = _single(cfg); + IntentData memory intentData = _getIntentData(0, type(uint128).max, condition); + _setUpMainAddress(intentData, false); + + ActionData memory actionData = _getActionData( + intentData.tokenData, _adjustRecipient(feeAfter == 0 ? swapdata2 : swapdata), false + ); + // oracleUpdateIndex = 0 (single condition); supply the real Pyth blob when any leg is Pyth + actionData.hookActionData = + abi.encode(uint256(0), (feeBefore << 128) | feeAfter, uint256(0), updateData); + + (address caller, bytes memory dk, bytes memory gd) = + _getCallerAndSignatures(mode, intentData, actionData); + + if (!ok) { + vm.startPrank(caller); + vm.expectRevert(KSConditionalSwapHook.InvalidSwap.selector); + router.execute(intentData, dk, guardian, gd, actionData); + return; + } + + uint256 balBefore = IERC20(tokenOut).balanceOf(mainAddress); + vm.startPrank(caller); + router.execute(intentData, dk, guardian, gd, actionData); + vm.stopPrank(); + assertGt(IERC20(tokenOut).balanceOf(mainAddress), balBefore); + } + + function _legIn(bool pyth_, uint256 band) internal pure returns (OracleLib.TokenOracle memory) { + return + pyth_ ? _pythLeg(PYTH_MAINNET, PYTH_USDT_USD, band) : _chainlinkLeg(CHAINLINK_USDT_USD, band); + } + + function _legOut(bool pyth_, uint256 band) internal pure returns (OracleLib.TokenOracle memory) { + return + pyth_ ? _pythLeg(PYTH_MAINNET, PYTH_BTC_USD, band) : _chainlinkLeg(CHAINLINK_WBTC_USD, band); + } + + /// @dev Chainlink and Pyth agree on the live BTC price within 1%. + function test_Fork_RealOracles_Agree() public view { + (,, uint256 clRatio) = _readReal(_realChainlink(FULL_BAND, FULL_BAND)); + (,, uint256 pythRatio) = _readReal(_realPyth(FULL_BAND, FULL_BAND)); + uint256 diff = clRatio > pythRatio ? clRatio - pythRatio : pythRatio - clRatio; + assertLt(diff * 10_000, clRatio * 100); + } + + // --------------------------------------------------------------------------- + // Helpers + // --------------------------------------------------------------------------- + + function _realChainlink(uint256 bandIn, uint256 bandOut) + internal + pure + returns (OracleLib.OracleConfig memory) + { + return _config( + _chainlinkLeg(CHAINLINK_USDT_USD, bandIn), _chainlinkLeg(CHAINLINK_WBTC_USD, bandOut), 0 + ); + } + + function _realPyth(uint256 bandIn, uint256 bandOut) + internal + pure + returns (OracleLib.OracleConfig memory) + { + return _config( + _pythLeg(PYTH_MAINNET, PYTH_USDT_USD, bandIn), + _pythLeg(PYTH_MAINNET, PYTH_BTC_USD, bandOut), + 0 + ); + } + + function _readReal(OracleLib.OracleConfig memory cfg) + internal + view + returns (uint256 priceIn, uint256 priceOut, uint256 ratio) + { + return OracleLib.getPrices(cfg, tokenIn, tokenOut); + } + + function _oracleCondition(OracleLib.OracleConfig memory oracle) + internal + pure + returns (KSConditionalSwapHook.SwapCondition memory) + { + return KSConditionalSwapHook.SwapCondition({ + swapLimit: 4, + timeLimits: (0 << 128) | type(uint128).max, + amountInLimits: (0 << 128) | type(uint128).max, + maxFees: (0 << 128) | type(uint128).max, + priceLimits: (0 << 128) | type(uint128).max, + oracle: oracle + }); + } + + function _single(OracleLib.OracleConfig memory oracle) + internal + pure + returns (KSConditionalSwapHook.SwapCondition[] memory conditions) + { + conditions = new KSConditionalSwapHook.SwapCondition[](1); + conditions[0] = _oracleCondition(oracle); + } + + function _timeCondition(uint256 timeLimits) + internal + view + returns (KSConditionalSwapHook.SwapCondition memory) + { + return KSConditionalSwapHook.SwapCondition({ + swapLimit: 1, + timeLimits: timeLimits, + amountInLimits: (swapAmount << 128) | swapAmount, + maxFees: (0 << 128) | type(uint128).max, + priceLimits: (0 << 128) | type(uint128).max, + oracle: _noOracle() + }); + } + + /// @dev amountOut that yields a realized price of `realizedPrice` for amountIn == swapAmount. + function _amountOutFor(uint256 realizedPrice) internal view returns (uint256) { + return (realizedPrice * swapAmount) / 1e18; + } + + function _pythUpdateData(uint256 publishTime) internal pure returns (bytes[] memory updateData) { + updateData = new bytes[](2); + updateData[0] = abi.encode(USDT_ID, int64(1e8), int32(-8), publishTime); + updateData[1] = abi.encode(WBTC_ID, int64(int256(100_000e8)), int32(-8), publishTime); + } + + function _mockSwapAction() internal view returns (ActionData memory actionData) { + TokenData memory tokenData; + tokenData.erc20Data = new ERC20Data[](1); + tokenData.erc20Data[0] = ERC20Data({token: tokenIn, amount: swapAmount, permitData: ''}); + actionData = _getActionData( + tokenData, + abi.encode( + tokenIn, + tokenOut, + swapAmount, + 1000, + feeAfter == 0 ? mainAddress : address(router), + mainAddress + ), + true + ); + } + + function _buildIntentAndAction( + KSConditionalSwapHook.SwapCondition[] memory conditions, + uint256 amountOut, + uint256 oracleUpdateIndex, + bytes[] memory updateData + ) internal returns (IntentData memory intentData, ActionData memory actionData) { + { + uint256 tmp = swapAmount; + swapAmount = type(uint256).max; + intentData = _getIntentData(0, type(uint128).max, conditions); + _setUpMainAddress(intentData, false); + swapAmount = tmp; + } + + TokenData memory tokenData; + tokenData.erc20Data = new ERC20Data[](1); + tokenData.erc20Data[0] = ERC20Data({token: tokenIn, amount: swapAmount, permitData: ''}); + + actionData = _getActionData( + tokenData, + abi.encode(tokenIn, tokenOut, swapAmount, amountOut, mainAddress, mainAddress), + true + ); + actionData.hookActionData = abi.encode(uint256(0), uint256(0), oracleUpdateIndex, updateData); + } + + function _expectSwapOk( + uint256 mode, + OracleLib.OracleConfig memory cfg, + uint256 amountOut, + uint256 oracleUpdateIndex, + bytes[] memory updateData + ) internal { + (IntentData memory intentData, ActionData memory actionData) = + _buildIntentAndAction(_single(cfg), amountOut, oracleUpdateIndex, updateData); + uint256 balBefore = IERC20(tokenOut).balanceOf(mainAddress); + _executeSwap(mode, intentData, actionData); + assertGt(IERC20(tokenOut).balanceOf(mainAddress), balBefore); + } + + function _expectSwapRevert( + uint256 mode, + OracleLib.OracleConfig memory cfg, + uint256 amountOut, + uint256 oracleUpdateIndex, + bytes[] memory updateData + ) internal { + (IntentData memory intentData, ActionData memory actionData) = + _buildIntentAndAction(_single(cfg), amountOut, oracleUpdateIndex, updateData); + (address caller, bytes memory dk, bytes memory gd) = + _getCallerAndSignatures(mode, intentData, actionData); + vm.startPrank(caller); + vm.expectRevert(KSConditionalSwapHook.InvalidSwap.selector); + router.execute(intentData, dk, guardian, gd, actionData); + } + + function _executeSwap(uint256 mode, IntentData memory intentData, ActionData memory actionData) + internal + { + (address caller, bytes memory dkSignature, bytes memory gdSignature) = + _getCallerAndSignatures(mode, intentData, actionData); + vm.startPrank(caller); + router.execute(intentData, dkSignature, guardian, gdSignature, actionData); + vm.stopPrank(); + } + + function _swap( + uint256 mode, + IntentData memory intentData, + ActionData memory actionData, + uint256 swapCount, + uint256 index + ) internal { + (address caller, bytes memory dkSignature, bytes memory gdSignature) = + _getCallerAndSignatures(mode, intentData, actionData); + bytes32 hash = router.hashTypedIntentData(intentData); + + uint256 balanceBefore = tokenOut.balanceOf(mainAddress); + + assertEq(conditionalSwapHook.getSwapExecutionCount(hash, 0, index), swapCount); + vm.startPrank(caller); + router.execute(intentData, dkSignature, guardian, gdSignature, actionData); + vm.stopPrank(); + assertEq(conditionalSwapHook.getSwapExecutionCount(hash, 0, index), swapCount + 1); + + assertGt(tokenOut.balanceOf(mainAddress), balanceBefore); + } + function _getActionData(TokenData memory tokenData, bytes memory actionCalldata, bool swapViaMock) internal view @@ -487,7 +935,9 @@ contract ConditionalSwapTest is BaseTest { ) : actionCalldata) : actionCalldata, - hookActionData: abi.encode(0, (feeBefore << 128) | feeAfter), + hookActionData: abi.encode( + uint256(0), (feeBefore << 128) | feeAfter, uint256(0), new bytes[](0) + ), extraData: '', deadline: vm.getBlockTimestamp() + 1 days, nonce: 0 @@ -514,7 +964,8 @@ contract ConditionalSwapTest is BaseTest { timeLimits: (vm.getBlockTimestamp() << 128) | (vm.getBlockTimestamp() + 1 days), amountInLimits: (min << 128) | max, maxFees: (maxSrcFee << 128) | maxDstFee, - priceLimits: (0 << 128) | type(uint128).max + priceLimits: (0 << 128) | type(uint128).max, + oracle: _noOracle() }); } @@ -542,36 +993,6 @@ contract ConditionalSwapTest is BaseTest { vm.stopPrank(); } - function _getActionData(TokenData memory tokenData, bytes memory actionCalldata) - internal - view - returns (ActionData memory actionData) - { - actionData.feeInfo.protocolRecipient = protocolRecipient; - actionData.feeInfo.partnerFeeConfigs = new FeeConfig[][](1); - actionData.feeInfo.partnerFeeConfigs[0] = _buildPartnersConfigs( - PartnersFeeConfigBuildParams({ - feeModes: [false].toMemoryArray(), - partnerFees: [uint24(1e6)].toMemoryArray(), - partnerRecipients: [partnerRecipient].toMemoryArray() - }) - ); - - actionData = ActionData({ - erc20Ids: [uint256(0)].toMemoryArray(), - erc20Amounts: [tokenData.erc20Data[0].amount].toMemoryArray(), - erc721Ids: new uint256[](0), - feeInfo: actionData.feeInfo, - approvalFlags: (1 << (tokenData.erc20Data.length + tokenData.erc721Data.length)) - 1, - actionSelectorId: 0, - actionCalldata: actionCalldata, - hookActionData: abi.encode(0), - extraData: '', - deadline: vm.getBlockTimestamp() + 1 days, - nonce: 0 - }); - } - function _adjustRecipient(bytes memory data) internal view returns (bytes memory) { IKSSwapRouterV2.SwapExecutionParams memory params = abi.decode(data, (IKSSwapRouterV2.SwapExecutionParams)); diff --git a/test/mocks/MockChainlinkFeed.sol b/test/mocks/MockChainlinkFeed.sol new file mode 100644 index 0000000..4a915a3 --- /dev/null +++ b/test/mocks/MockChainlinkFeed.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.0; + +import {AggregatorV3Interface} from 'src/interfaces/oracle/external/AggregatorV3Interface.sol'; + +contract MockChainlinkFeed is AggregatorV3Interface { + uint8 internal _decimals; + int256 public answer; + uint256 public updatedAt; + + constructor(uint8 decimals_, int256 answer_) { + _decimals = decimals_; + answer = answer_; + updatedAt = block.timestamp; + } + + function decimals() external view returns (uint8) { + return _decimals; + } + + function setAnswer(int256 answer_) external { + answer = answer_; + updatedAt = block.timestamp; + } + + function setUpdatedAt(uint256 updatedAt_) external { + updatedAt = updatedAt_; + } + + function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) { + return (1, answer, updatedAt, updatedAt, 1); + } +} diff --git a/test/mocks/MockPyth.sol b/test/mocks/MockPyth.sol new file mode 100644 index 0000000..4539a3c --- /dev/null +++ b/test/mocks/MockPyth.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.0; + +import {IPyth} from 'src/interfaces/oracle/external/IPyth.sol'; + +contract MockPyth is IPyth { + error InsufficientFee(); + error StalePrice(); + error NoPrice(); + + uint256 public fee; + uint256 public updateCount; + mapping(bytes32 => Price) internal _prices; + + constructor(uint256 fee_) { + fee = fee_; + } + + function setPrice(bytes32 id, int64 price, int32 expo, uint256 publishTime) external { + _prices[id] = Price({price: price, conf: 0, expo: expo, publishTime: publishTime}); + } + + function getUpdateFee(bytes[] calldata) external view returns (uint256) { + return fee; + } + + function updatePriceFeeds(bytes[] calldata updateData) external payable { + if (msg.value < fee) revert InsufficientFee(); + updateCount++; + for (uint256 i; i < updateData.length; ++i) { + (bytes32 id, int64 price, int32 expo, uint256 publishTime) = + abi.decode(updateData[i], (bytes32, int64, int32, uint256)); + _prices[id] = Price({price: price, conf: 0, expo: expo, publishTime: publishTime}); + } + } + + function getPriceNoOlderThan(bytes32 id, uint256 age) external view returns (Price memory) { + Price memory price = _prices[id]; + if (price.publishTime == 0) revert NoPrice(); + if (block.timestamp - price.publishTime > age) revert StalePrice(); + return price; + } +} From 455e81234a8f4221701384e4febcde73b1607efd Mon Sep 17 00:00:00 2001 From: minhtr09 <22521523@gm.uit.edu.vn> Date: Wed, 24 Jun 2026 18:01:37 +0700 Subject: [PATCH 2/2] fix: cmts --- src/hooks/swap/KSConditionalSwapHook.sol | 107 +++--- src/libraries/OracleLib.sol | 78 ++-- src/types/PackedU128.sol | 47 +++ test/ConditionalSwap.t.sol | 448 ++++++++++++----------- 4 files changed, 354 insertions(+), 326 deletions(-) create mode 100644 src/types/PackedU128.sol diff --git a/src/hooks/swap/KSConditionalSwapHook.sol b/src/hooks/swap/KSConditionalSwapHook.sol index f8c7246..d328e43 100644 --- a/src/hooks/swap/KSConditionalSwapHook.sol +++ b/src/hooks/swap/KSConditionalSwapHook.sol @@ -5,6 +5,7 @@ import {IKSSmartIntentHook} from '../../interfaces/hooks/IKSSmartIntentHook.sol' import {OracleLib} from '../../libraries/OracleLib.sol'; import {ActionData} from '../../types/ActionData.sol'; import {IntentData} from '../../types/IntentData.sol'; +import {PackedU128, PackedU128Library} from '../../types/PackedU128.sol'; import {BaseStatefulHook} from '../base/BaseStatefulHook.sol'; import {CalldataDecoder} from 'ks-common-sc/src/libraries/calldata/CalldataDecoder.sol'; import {TokenHelper} from 'ks-common-sc/src/libraries/token/TokenHelper.sol'; @@ -46,10 +47,10 @@ contract KSConditionalSwapHook is BaseStatefulHook { */ struct SwapCondition { uint8 swapLimit; - uint256 timeLimits; - uint256 amountInLimits; - uint256 maxFees; - uint256 priceLimits; + PackedU128 timeLimits; + PackedU128 amountInLimits; + PackedU128 maxFees; + PackedU128 priceLimits; OracleLib.OracleConfig oracle; } @@ -94,50 +95,44 @@ contract KSConditionalSwapHook is BaseStatefulHook { ActionData calldata actionData ) external + view override onlyWhitelistedRouter checkTokenLengths(actionData) returns (uint256[] memory fees, bytes memory beforeExecutionData) { - SwapHookData calldata swapHookData = _decodeHookData(intentData.coreData.hookIntentData); - ( - uint256 index, - uint256 intentSrcFee, - uint256 intentDstFee, - uint256 oracleUpdateIndex, - bytes[] calldata updateData - ) = _decodeHookActionData(actionData.hookActionData); + (uint256 index, uint256 intentSrcFee, uint256 intentDstFee) = + _decodeHookActionData(actionData.hookActionData); address tokenIn = intentData.tokenData.erc20Data[actionData.erc20Ids[0]].token; - address tokenOut = swapHookData.dstTokens[index]; - uint256 amountIn = actionData.erc20Amounts[0]; - - require( - tokenIn == swapHookData.srcTokens[index], - InvalidTokenIn(tokenIn, swapHookData.srcTokens[index]) - ); - - if (updateData.length != 0) { - OracleLib.refresh(swapHookData.swapConditions[index][oracleUpdateIndex].oracle, updateData); + address tokenOut; + address recipient; + // prevent stack too deep + { + SwapHookData calldata swapHookData = _decodeHookData(intentData.coreData.hookIntentData); + tokenOut = swapHookData.dstTokens[index]; + recipient = swapHookData.recipient; + + require( + tokenIn == swapHookData.srcTokens[index], + InvalidTokenIn(tokenIn, swapHookData.srcTokens[index]) + ); } + uint256 amountIn = actionData.erc20Amounts[0]; fees = new uint256[](1); fees[0] = (amountIn * intentSrcFee) / PRECISION; beforeExecutionData = abi.encode( - SwapValidationData({ - intentHash: intentHash, - intentIndex: index, - tokenIn: tokenIn, - tokenOut: tokenOut, - amountIn: amountIn, - srcFeePercent: intentSrcFee, - dstFeePercent: intentDstFee, - recipientBalanceBefore: _getRecipientBalance( - tokenOut, swapHookData.recipient, intentDstFee - ), // if dstFee is 0, transfer directly to the recipient - swapperBalanceBefore: tokenIn.balanceOf(intentData.coreData.mainAddress), - recipient: swapHookData.recipient - }) + intentHash, + index, + tokenIn, + tokenOut, + amountIn, + _getRecipientBalance(tokenOut, recipient, intentDstFee), + tokenIn.balanceOf(intentData.coreData.mainAddress), + intentSrcFee, + intentDstFee, + recipient ); return (fees, beforeExecutionData); @@ -237,24 +232,23 @@ contract KSConditionalSwapHook is BaseStatefulHook { for (uint256 i; i < swapCondition.length; ++i) { SwapCondition calldata condition = swapCondition[i]; - if ( - block.timestamp < condition.timeLimits >> 128 - || block.timestamp > uint128(condition.timeLimits) - ) { + (uint128 minTime, uint128 maxTime) = condition.timeLimits.unpack(); + if (block.timestamp < minTime || block.timestamp > maxTime) { continue; } - if ( - amountIn < condition.amountInLimits >> 128 || amountIn > uint128(condition.amountInLimits) - ) { + (uint128 minAmountIn, uint128 maxAmountIn) = condition.amountInLimits.unpack(); + if (amountIn < minAmountIn || amountIn > maxAmountIn) { continue; } - if (srcFeePercent > condition.maxFees >> 128 || dstFeePercent > uint128(condition.maxFees)) { + (uint128 maxSrcFee, uint128 maxDstFee) = condition.maxFees.unpack(); + if (srcFeePercent > maxSrcFee || dstFeePercent > maxDstFee) { continue; } - if (price < condition.priceLimits >> 128 || price > uint128(condition.priceLimits)) { + (uint128 minPrice, uint128 maxPrice) = condition.priceLimits.unpack(); + if (price < minPrice || price > maxPrice) { continue; } @@ -322,29 +316,16 @@ contract KSConditionalSwapHook is BaseStatefulHook { } } - // @dev: equivalent to abi.encode(index, packedFees, oracleUpdateIndex, bytes[] updateData). + // @dev: equivalent to abi.encode(index, packedFees). function _decodeHookActionData(bytes calldata data) internal pure - returns ( - uint256 index, - uint256 intentSrcFee, - uint256 intentDstFee, - uint256 oracleUpdateIndex, - bytes[] calldata updateData - ) + returns (uint256 index, uint256 intentSrcFee, uint256 intentDstFee) { index = data.decodeUint256(0); - intentSrcFee = data.decodeUint256(1); - oracleUpdateIndex = data.decodeUint256(2); - (uint256 length, uint256 offset) = data.decodeLengthOffset(3); - assembly ('memory-safe') { - updateData.length := length - updateData.offset := offset - } - - intentDstFee = uint128(intentSrcFee); - intentSrcFee = intentSrcFee >> 128; + (uint128 srcFee, uint128 dstFee) = PackedU128.wrap(data.decodeUint256(1)).unpack(); + intentSrcFee = srcFee; + intentDstFee = dstFee; } // @dev: equivalent to abi.decode(data, (SwapValidationData)) diff --git a/src/libraries/OracleLib.sol b/src/libraries/OracleLib.sol index d259374..3821331 100644 --- a/src/libraries/OracleLib.sol +++ b/src/libraries/OracleLib.sol @@ -6,10 +6,10 @@ import {Math} from 'openzeppelin-contracts/contracts/utils/math/Math.sol'; import {AggregatorV3Interface} from '../interfaces/oracle/external/AggregatorV3Interface.sol'; import {IPyth} from '../interfaces/oracle/external/IPyth.sol'; +import {PackedU128, PackedU128Library} from '../types/PackedU128.sol'; library OracleLib { - uint256 internal constant PRICE_PRECISION = 1e18; - uint256 internal constant BPS_DENOMINATOR = 10_000; + uint256 internal constant PRECISION = 1e18; enum OracleType { NONE, @@ -28,44 +28,29 @@ library OracleLib { OracleType oracleType; address source; bytes32 priceId; - uint256 priceLimits; + PackedU128 priceLimits; } /** * @param oracleIn Input-token oracle. * @param oracleOut Output-token oracle. - * @param oracleParams maxStaleness 128bits | maxDeviationBps 128bits (0 disables slippage guard). + * @param oracleParams maxStaleness 128bits | maxDeviation 128bits, scaled by 1e18 (0 disables slippage guard). */ struct OracleConfig { TokenOracle oracleIn; TokenOracle oracleOut; - uint256 oracleParams; + PackedU128 oracleParams; } error InvalidOraclePrice(); + error InvalidMaxDeviation(); error StaleOraclePrice(); - /** - * @notice Pushes the Pyth update - */ - function refresh(OracleConfig memory config, bytes[] calldata updateData) internal { - if (updateData.length == 0) return; - address pythIn = - config.oracleIn.oracleType == OracleType.PYTH ? config.oracleIn.source : address(0); - address pythOut = - config.oracleOut.oracleType == OracleType.PYTH ? config.oracleOut.source : address(0); - if (pythIn != address(0)) { - _pythUpdate(pythIn, updateData); - } - if (pythOut != address(0) && pythOut != pythIn) { - _pythUpdate(pythOut, updateData); - } - } - /** * @notice Validates a swap. Each configured leg's oracle price must sit in its band; an - * unconfigured leg is skipped. The slippage guard (realized within `maxDeviationBps` of - * the oracle ratio) applies only when both legs are set. True if no leg is set. + * unconfigured leg is skipped. The slippage guard ensures the realized price does not + * fall more than `maxDeviation` below the oracle ratio; better execution is allowed. + * It applies only when both legs are set. True if no leg is set. * @param realizedPrice Realized swap price, raw basis (`amountOut_raw * 1e18 / amountIn_raw`). */ function validate( @@ -74,40 +59,39 @@ library OracleLib { address tokenOut, uint256 realizedPrice ) internal view returns (bool) { + (uint128 maxStaleness, uint128 maxDeviation) = config.oracleParams.unpack(); + require(maxDeviation <= PRECISION, InvalidMaxDeviation()); + bool hasIn = config.oracleIn.source != address(0); bool hasOut = config.oracleOut.source != address(0); if (!hasIn && !hasOut) return true; // no oracle configured - uint256 maxStaleness = config.oracleParams >> 128; uint256 priceIn; uint256 priceOut; // market-price trigger: only configured legs are read and bounded if (hasIn) { priceIn = _usdPrice(config.oracleIn, maxStaleness); - if ( - priceIn < config.oracleIn.priceLimits >> 128 - || priceIn > uint128(config.oracleIn.priceLimits) - ) { + (uint128 minPriceIn, uint128 maxPriceIn) = config.oracleIn.priceLimits.unpack(); + if (priceIn < minPriceIn || priceIn > maxPriceIn) { return false; } } if (hasOut) { priceOut = _usdPrice(config.oracleOut, maxStaleness); - if ( - priceOut < config.oracleOut.priceLimits >> 128 - || priceOut > uint128(config.oracleOut.priceLimits) - ) { + (uint128 minPriceOut, uint128 maxPriceOut) = config.oracleOut.priceLimits.unpack(); + if (priceOut < minPriceOut || priceOut > maxPriceOut) { return false; } } // slippage guard: needs both legs to derive the ratio - uint256 maxDeviationBps = uint128(config.oracleParams); - if (maxDeviationBps != 0 && hasIn && hasOut) { + if (maxDeviation != 0 && hasIn && hasOut) { uint256 ratio = _ratio(priceIn, priceOut, tokenIn, tokenOut); - uint256 diff = realizedPrice > ratio ? realizedPrice - ratio : ratio - realizedPrice; - if (diff * BPS_DENOMINATOR > maxDeviationBps * ratio) return false; + if (maxDeviation < PRECISION) { + uint256 minRealizedPrice = Math.mulDiv(ratio, PRECISION - maxDeviation, PRECISION); + if (realizedPrice < minRealizedPrice) return false; + } } return true; @@ -119,7 +103,7 @@ library OracleLib { view returns (uint256 priceIn, uint256 priceOut, uint256 ratio) { - uint256 maxStaleness = config.oracleParams >> 128; + (uint128 maxStaleness,) = config.oracleParams.unpack(); priceIn = _usdPrice(config.oracleIn, maxStaleness); priceOut = _usdPrice(config.oracleOut, maxStaleness); ratio = _ratio(priceIn, priceOut, tokenIn, tokenOut); @@ -134,15 +118,9 @@ library OracleLib { uint8 decimalsIn = IERC20Metadata(tokenIn).decimals(); uint8 decimalsOut = IERC20Metadata(tokenOut).decimals(); if (decimalsOut >= decimalsIn) { - return - Math.mulDiv(priceIn, PRICE_PRECISION * (10 ** uint256(decimalsOut - decimalsIn)), priceOut); + return Math.mulDiv(priceIn, PRECISION * (10 ** uint256(decimalsOut - decimalsIn)), priceOut); } - return - Math.mulDiv(priceIn, PRICE_PRECISION, priceOut * (10 ** uint256(decimalsIn - decimalsOut))); - } - - function _pythUpdate(address pyth, bytes[] calldata updateData) private { - IPyth(pyth).updatePriceFeeds{value: IPyth(pyth).getUpdateFee(updateData)}(updateData); + return Math.mulDiv(priceIn, PRECISION, priceOut * (10 ** uint256(decimalsIn - decimalsOut))); } /// @dev Token oracle price, USD-per-whole-token (1e18). @@ -155,16 +133,16 @@ library OracleLib { (, int256 answer,, uint256 updatedAt,) = AggregatorV3Interface(oracle.source).latestRoundData(); if (answer <= 0) revert InvalidOraclePrice(); - if (maxStaleness != 0 && block.timestamp - updatedAt > maxStaleness) { + if (block.timestamp - updatedAt > maxStaleness) { revert StaleOraclePrice(); } uint8 feedDecimals = AggregatorV3Interface(oracle.source).decimals(); - return Math.mulDiv(uint256(answer), PRICE_PRECISION, 10 ** feedDecimals); + return Math.mulDiv(uint256(answer), PRECISION, 10 ** feedDecimals); } // PYTH: price = pyth.price * 10^(expo + 18) - uint256 age = maxStaleness == 0 ? type(uint256).max : maxStaleness; - IPyth.Price memory price = IPyth(oracle.source).getPriceNoOlderThan(oracle.priceId, age); + IPyth.Price memory price = + IPyth(oracle.source).getPriceNoOlderThan(oracle.priceId, maxStaleness); if (price.price <= 0) revert InvalidOraclePrice(); int256 exponent = int256(price.expo) + 18; diff --git a/src/types/PackedU128.sol b/src/types/PackedU128.sol new file mode 100644 index 0000000..3792293 --- /dev/null +++ b/src/types/PackedU128.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import '../libraries/BitMask.sol'; + +/** + * @notice two 128-bit values packed into a 256-bit value + * where the first 128 bits are the first value + * and the last 128 bits are the second value. + */ +type PackedU128 is uint256; + +using PackedU128Library for PackedU128 global; + +/** + * @notice pack two 128-bit values into a 256-bit value + * @dev use 256-bit params for versatility + */ +function toPackedU128(uint256 value0, uint256 value1) pure returns (PackedU128 packedU128) { + assembly ('memory-safe') { + packedU128 := or(shl(128, value0), value1) + } +} + +library PackedU128Library { + /// @notice get the first 128 bits of the packed value + function value0(PackedU128 packedU128) internal pure returns (uint128 _value0) { + assembly ('memory-safe') { + _value0 := shr(128, packedU128) + } + } + + /// @notice get the last 128 bits of the packed value + function value1(PackedU128 packedU128) internal pure returns (uint128 _value1) { + assembly ('memory-safe') { + _value1 := and(packedU128, MASK_128_BITS) + } + } + + /// @notice unpack the packed value into two 128-bit values + function unpack(PackedU128 packedU128) internal pure returns (uint128 _value0, uint128 _value1) { + assembly ('memory-safe') { + _value0 := shr(128, packedU128) + _value1 := and(packedU128, MASK_128_BITS) + } + } +} diff --git a/test/ConditionalSwap.t.sol b/test/ConditionalSwap.t.sol index 7e7630a..ca27019 100644 --- a/test/ConditionalSwap.t.sol +++ b/test/ConditionalSwap.t.sol @@ -5,7 +5,9 @@ import './Base.t.sol'; import 'src/hooks/swap/KSConditionalSwapHook.sol'; +import {IPyth} from 'src/interfaces/oracle/external/IPyth.sol'; import {OracleLib} from 'src/libraries/OracleLib.sol'; +import {PackedU128, toPackedU128} from 'src/types/PackedU128.sol'; import {MockChainlinkFeed} from './mocks/MockChainlinkFeed.sol'; import {MockPyth} from './mocks/MockPyth.sol'; @@ -15,13 +17,10 @@ contract ConditionalSwapTest is BaseTest { using TokenHelper for address; using ArraysHelper for *; - bytes swapdata = - hex'00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000f4a1d7fdf4890be35e71f3e0bbc4a0ec377eca3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000007a000000000000000000000000000000000000000000000000000000000000009e000000000000000000000000000000000000000000000000000000000000006e0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec70000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c5990000000000000000000000002e234DAe75C793f67A35089C9d99245E1C58470b0000000000000000000000000000000000000000000000000000000067db987b00000000000000000000000000000000000000000000000000000000000006800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000040f59b1df7000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000002000000000000000000000000066a9893cc07d91d95644aedd05d03f95e1dba8af000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000000000000022d473030f116ddee9f6b43ac78ba3000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000000000002300000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040a9d4c672000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000180000000000000000000000000655edce464cc797526600a462a8154650eee4b77000000000000000000000000000000000000000000000000000000003b9d5f1a000000000000000000000000000000000000000000000000000000003b9d5f1a00000000000000000000000000000000000000000000000006dac07944b594800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000f4a1d7fdf4890be35e71f3e0bbc4a0ec377eca3000000000000005fa94793ea0000001a371930340fc8fbcc09c409c467db9414000000000000000000000000000000000000000000000000000000000000001bdcffd1bf68c2c17dcf00a25c935efba96aa63b7f75dd43d42b3df2cf7273c2260fb4b38a9db829fbfdabcc6262ac3982f1d31366bfde12a7b67f6f31ba52b2cb0000000000000000000000000000000000000000000000000000000000000040d90ce4910000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000001000000000000000000000000007f86bf177dd4f3494b841a37e810a34dd56c829b000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c5990000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000006da929a6bb58cc0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000010000000000000000000000000011cbb0000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec70000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c599000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002000000000000000000000000002e234DAe75C793f67A35089C9d99245E1C58470b000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000000000000000000000000000000000000011c7210000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000f4a1d7fdf4890be35e71f3e0bbc4a0ec377eca30000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024f7b22536f75726365223a22222c22416d6f756e74496e555344223a22313030302e31373135393231313738353037222c22416d6f756e744f7574555344223a22313030302e34373538333032323939323331222c22526566657272616c223a22222c22466c616773223a302c22416d6f756e744f7574223a2231313636323536222c2254696d657374616d70223a313734323434333436392c22526f7574654944223a2263383438663432632d326465322d343364382d623366372d636637366362666430363536222c22496e74656772697479496e666f223a7b224b65794944223a2231222c225369676e6174757265223a224e39426b4975436430714961362f4d64736635717a61657863436c3754413539426e4d70454741437a74432b5875325176494a36444c34476b7075746b636f627554395657357a42744e427a5463736b4e7768434662372f6f52675173676970424e693878716d323869524b3048496834527a70316457512f437737676a58375168653270313853506966492b7550674e5a34647a5a6a4461686b664d416852796d7765783233714942536a65565a6f44483932596a534b4e546176396f2f2f634754766476336a52555538536841763153464b55514b54515470682f4d4f71534f7370646c37306632714155705274566d7739434b4d383347726164506b55546f5854684a2f6c734e784561634267395a37617a363837394d366d31517538465a687237796374367a4242524a774171464e6646436a364b523969307a4e702f665a2b6876394b6970455341666d5078634e4d67773d3d227d7d0000000000000000000000000000000000'; - bytes swapdata2 = - hex'00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000f4a1d7fdf4890be35e71f3e0bbc4a0ec377eca3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000007a000000000000000000000000000000000000000000000000000000000000009e000000000000000000000000000000000000000000000000000000000000006e0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec70000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c5990000000000000000000000003674eD9c52D903C6c3A468592Ac27Fe71B3CD8490000000000000000000000000000000000000000000000000000000067db987b00000000000000000000000000000000000000000000000000000000000006800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000040f59b1df7000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000002000000000000000000000000066a9893cc07d91d95644aedd05d03f95e1dba8af000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000000000000022d473030f116ddee9f6b43ac78ba3000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000000000002300000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040a9d4c672000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000180000000000000000000000000655edce464cc797526600a462a8154650eee4b77000000000000000000000000000000000000000000000000000000003b9d5f1a000000000000000000000000000000000000000000000000000000003b9d5f1a00000000000000000000000000000000000000000000000006dac07944b594800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000f4a1d7fdf4890be35e71f3e0bbc4a0ec377eca3000000000000005fa94793ea0000001a371930340fc8fbcc09c409c467db9414000000000000000000000000000000000000000000000000000000000000001bdcffd1bf68c2c17dcf00a25c935efba96aa63b7f75dd43d42b3df2cf7273c2260fb4b38a9db829fbfdabcc6262ac3982f1d31366bfde12a7b67f6f31ba52b2cb0000000000000000000000000000000000000000000000000000000000000040d90ce4910000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000001000000000000000000000000007f86bf177dd4f3494b841a37e810a34dd56c829b000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c5990000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000006da929a6bb58cc0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000010000000000000000000000000011cbb0000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec70000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c599000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002000000000000000000000000003674eD9c52D903C6c3A468592Ac27Fe71B3CD849000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000000000000000000000000000000000000011c7210000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000f4a1d7fdf4890be35e71f3e0bbc4a0ec377eca30000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024f7b22536f75726365223a22222c22416d6f756e74496e555344223a22313030302e31373135393231313738353037222c22416d6f756e744f7574555344223a22313030302e34373538333032323939323331222c22526566657272616c223a22222c22466c616773223a302c22416d6f756e744f7574223a2231313636323536222c2254696d657374616d70223a313734323434333436392c22526f7574654944223a2263383438663432632d326465322d343364382d623366372d636637366362666430363536222c22496e74656772697479496e666f223a7b224b65794944223a2231222c225369676e6174757265223a224e39426b4975436430714961362f4d64736635717a61657863436c3754413539426e4d70454741437a74432b5875325176494a36444c34476b7075746b636f627554395657357a42744e427a5463736b4e7768434662372f6f52675173676970424e693878716d323869524b3048496834527a70316457512f437737676a58375168653270313853506966492b7550674e5a34647a5a6a4461686b664d416852796d7765783233714942536a65565a6f44483932596a534b4e546176396f2f2f634754766476336a52555538536841763153464b55514b54515470682f4d4f71534f7370646c37306632714155705274566d7739434b4d383347726164506b55546f5854684a2f6c734e784561634267395a37617a363837394d366d31517538465a687237796374367a4242524a774171464e6646436a364b523969307a4e702f665a2b6876394b6970455341666d5078634e4d67773d3d227d7d0000000000000000000000000000000000'; - - bytes pythUpdateData = - hex'504e41550100000003b801000000040d00276f5b9bbd57764e3c885b2a93e2ec99e4410dddea133e2bcf6e6d2aa52220782156d586e84d0e4cd3174b4bd6f1451953c7d876cf5064c87f55ce2a661e8fee0102d23f5a5f808768e508584832f35dc3596d32630b52a05ff2f71e33d4d5333aa5672b33b488bb34d369258b1a49145244773a70e60d30306251b1634d2324bb2d01031056f9f2942476f131cbedc5b44d5582fd32b83c0ba9383b478e80c959e31af01fa88f50a33aeb7ab729d69a07d83fcba54f8f554e9c4060859ab762d3bff8f30004358c5fafc18f869fe8c1db5e2ebe2bdc8e501b8ad6c295740dd95eb878afd31358608385a66fdeeadc4c432560b51dfd2e1f57724e74bb3dd84197f0851068dc0106d9cc8e4f18b56badaf8e2f09ba38bfeadf5629780ff6ea3a396b4a3d631a1fce045552446529688efdc9c70d433faf55fbd10cf4a10d6aaa41d789eec29d30d90008877c385d9bfc1c69dcb82274cd076fc58086fb1e5b22eb50e5e11881e575125646de23f3656202b81402b3ab748b868fdce8abb5770a4e59c0a47e8794b2406c010a0f95e7560379535f9915806a8030dd90d732efdc9149a693ca611081b4ad01956cae625662a018eaa379bebcc75a58abff2334d8d21f736cba254c589ced4f5d010b48b5be572a2cadfc1fd0f74c23d7a8b43629e7c3600d1caa122d3932a370a9917a7ae16986001b931ad19a7167fc060ba3c328197d764f5a0f40f2c4c659c84e010cbd3cb104da104d83fb030623b292c046e8c7ab876866e9bf9444c31aa275f09b362e0cd2caf5d63fc67a4b4582351c00048258fc222fdcafd3a4f5cd973becdd000d6eb0c29f38df7fec3c52307caccf275999aded023f9d79048b17f98e184b6ac02dcdbd93dd6d146b59324d12f486336b110a2f814ff66c1ff299ff1427892b45000e5234724fad66ff9688510b538ad695dc8bcdce669b38f72fd6e0a276f173cc68571930fb308a047df9bb7f06a557b563d58484ca9d9e2bcecc387df20e3dd4d3010fd87f033e76d841cdf14bd2624008ef4f511bde4428590ba9f9fa782073afd96d744881c9bf908d6475b47d24dede2c5ab06c91c7b4df3b7a4b5c9818ec43e4f90010133189f049dcd388beb6a4063c9eeea9a2a54476efb79c3d39628e356673b278602849af9b6a0b9adbab8e72f533a5e9646441f813d8c8c37e985b5c064ad81a0067db904300000000001ae101faedac5851e32b9b23b5f9411a8c2bac4aae3ed4dd7b811dd1a72ea4aa7100000000072d7058014155575600000000000c3a7d8c00002710a12a5869dbbb38a8795bc72942ca40da9e0a5bd8020055002b89b9dc8fdf9f34709a5b106b472f0f39bb6ca9ce04b0fd7f2e971688e2e53b0000000005f6114f000000000000cda2fffffff80000000067db90430000000067db90420000000005f62653000000000000c5fa0cf52e2fe61280a39fe1b0710570a8df3293aaa7de950fbd77cb5a9fbd59efa8dae4ab333f4623a6936389052ab8b87098037e3541b7f5129257ad431e1806877866609c1fe71baae65c5c37c57825175aebe80f309cd48099b0040d6fb38f2dabac57e0717fb9b2ab916b0a94b4e931461438894d93a442dc6ccf8fb68da2a537171a52cfde7dea9243ee43193dcd7446429a85eb799e62d4fd72c56961aa0e0e33ce0488bd5938e05e1b4827d468d1732ba3e438bd99e2f0078e28842b59ee0f82f3cdf61b5ba8551bed77011a454079dba4ec47ddd6fd94450dc2c0928a53ac035b7de3792b6fe71bbd489f67c1e6aa005500e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43000007d16accffee00000000b1996851fffffff80000000067db90430000000067db9042000007d28b80ca4000000000bb14fbe80c73f40b6db4e39c00925c7d4c84e1da7a8f37bd95eb84812d210638850ea96d6b329964e81f75212ebdf8e7c5675b3449ba832014978ffa3f1aed4deb93c5355b06ab4a3cacc8e34f7f6e8e80e7a10de0f35cf6decc07c0b0409c8df3c3c90825d03d32e967b339885cf7dccc198e304ad02c5aa511e41519b898676021f3d4b43202ac5c01dc85a1fa3eecc7b649a1ca1a59b6e052cfd234141ab494bf1316b43c9bde3d074cea1b35d3726f16e6e3d8189b8aedc64c1fce8b2ad65ec2d36e0332101da6b69b19325a8d91369a0974404bce4596d217d9008dbe3217928a53ac035b7de3792b6fe71bbd489f67c1e6aa'; + bytes constant swapdata = + hex'00000000000000000000000000000000000000000000000000000000000000200000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f996000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000ea000000000000000000000000000000000000000000000000000000000000010e00000000000000000000000000000000000000000000000000000000000000de00000000000000000000000003b9aca000000000000000000000000003b9aca00000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000041f1f7da89549910ed771b7001fef5741ed5950ead9cb23d0ecdf258e155abb35705bc055478f721f1a272600062d4da9c47e4d9559002535906b09cb2cced06d71b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ce00000000000000000000000003674ed9c52d903c6c3a468592ac27fe71b3cd849000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001a0000000000000000000000000389fd9800000000000000000000000003e95ba800000000000000000000000003b9aca0000000000000000000000000000185b2c00000000000000000000000000000000000000000000010000000f42400000000000000000000000000000004f82e73edb06d29ff62c91ec8f5ff06571bdeb290000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006a3cf4660000000000000000000000000000000000000000000000000000000000000cc0000000000000000000000000000000000000000000000000000000000000000261f598cd000000000000000031439a79a3535d69b65c3be384840282b4ea0aa791dd7346000000000000000031439a79a3535d69b65c3be384840282b4ea0aa7000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000005e000000000000000000000000000000000000000000000000000000000000007200000000000000000000000000000000000000000000000000000000000000a80000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7800000000000000000000000000003e80000000000000000000000003b9aca00000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000003b9aca0008fce00900000000000000015455c918e405a2831fbff8595c0aae35ee3db9d100000000000000000000000000000000000000000000000000000000000000800000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f99600000000000000000000000000000000000000000000000000000000000000200000000000000000000001004f493b7de8aac7d55f71853688b1f7c8f0243c85000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48800000000000000000000000000003e60000000000000000000000003b8a9c13000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000003b8a9c134f2d31ea00000000000000025455c918e405a2831fbff8595c0aae35ee3db9d100000000000000000000000000000000000000000000000000000000000000800000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f99600000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000008514a940968a73f0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e03b9d6e0900000000000000005455c918e405a2831fbff8595c0aae35ee3db9d139072a4500000000000000001b70046376a3db8b129409f5643ac9474a3be8130000000000000000000000000000000000000000000000000000000000000040000000000000000000000000e0554a476a092703abdb3ef35c80e0d76d32939f00000000000000000000000000000000000000000000000000000001000276a40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0c22a39f70000000000000000e4298cbc7f2bfe93d0503cb36b214224691f2343000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000005cdbe59400cc2efdcc2b54acca4a99fe00dd588c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc280000000000000000000008b8b9496c2000000000000000008514a940968a73f00000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000008514a940968a73f0000000000000000000000030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee80000000000000000000008b8b9496c2000000000000000008514a940968a73f00000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000008514a940968a73f736e774d0000000000000004d1877a31a73c7cb31c02b9e7d7c336531562b21e00000000000000000000000000000000000000000000000000000000000000800000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f9960000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000004444c5dc75cb358380d2e3de08a900000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008514a940968a73f0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c59900000000000000000000000000000000000000000000000000000000000001f4000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000010009046d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c5998000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec70000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c599000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002000000000000000000000000003674ed9c52d903c6c3a468592ac27fe71b3cd849000000000000000000000000000000000000000000000000000000003b9aca000000000000000000000000000000000000000000000000000000000000181cd00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000000010000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f9960000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000003b9aca0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ad7b22536f75726365223a226b79626572737761702d74657374222c22416d6f756e74496e555344223a223939382e373638313539222c22416d6f756e744f7574555344223a223939382e363836343138222c22416d6f756e744f7574223a2231353936323033222c22526f7574654944223a2266653665646334385164424d667075533a396232623230336139794a4b56374157222c2254696d657374616d70223a313738323239333232377d00000000000000000000000000000000000000'; + bytes constant swapdata2 = + hex'00000000000000000000000000000000000000000000000000000000000000200000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f996000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000ea000000000000000000000000000000000000000000000000000000000000010e00000000000000000000000000000000000000000000000000000000000000de00000000000000000000000003b9aca000000000000000000000000003b9aca00000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000041f1f7da89549910ed771b7001fef5741ed5950ead9cb23d0ecdf258e155abb35705bc055478f721f1a272600062d4da9c47e4d9559002535906b09cb2cced06d71b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ce00000000000000000000000003674ed9c52d903c6c3a468592ac27fe71b3cd849000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001a0000000000000000000000000389fd9800000000000000000000000003e95ba800000000000000000000000003b9aca0000000000000000000000000000185b2c00000000000000000000000000000000000000000000010000000f42400000000000000000000000000000004f82e73edb06d29ff62c91ec8f5ff06571bdeb290000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006a3cf4660000000000000000000000000000000000000000000000000000000000000cc0000000000000000000000000000000000000000000000000000000000000000261f598cd000000000000000031439a79a3535d69b65c3be384840282b4ea0aa791dd7346000000000000000031439a79a3535d69b65c3be384840282b4ea0aa7000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000005e000000000000000000000000000000000000000000000000000000000000007200000000000000000000000000000000000000000000000000000000000000a80000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7800000000000000000000000000003e80000000000000000000000003b9aca00000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000003b9aca0008fce00900000000000000015455c918e405a2831fbff8595c0aae35ee3db9d100000000000000000000000000000000000000000000000000000000000000800000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f99600000000000000000000000000000000000000000000000000000000000000200000000000000000000001004f493b7de8aac7d55f71853688b1f7c8f0243c85000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48800000000000000000000000000003e60000000000000000000000003b8a9c13000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000003b8a9c134f2d31ea00000000000000025455c918e405a2831fbff8595c0aae35ee3db9d100000000000000000000000000000000000000000000000000000000000000800000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f99600000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000008514a940968a73f0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e03b9d6e0900000000000000005455c918e405a2831fbff8595c0aae35ee3db9d139072a4500000000000000001b70046376a3db8b129409f5643ac9474a3be8130000000000000000000000000000000000000000000000000000000000000040000000000000000000000000e0554a476a092703abdb3ef35c80e0d76d32939f00000000000000000000000000000000000000000000000000000001000276a40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0c22a39f70000000000000000e4298cbc7f2bfe93d0503cb36b214224691f2343000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000005cdbe59400cc2efdcc2b54acca4a99fe00dd588c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc280000000000000000000008b8b9496c2000000000000000008514a940968a73f00000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000008514a940968a73f0000000000000000000000030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee80000000000000000000008b8b9496c2000000000000000008514a940968a73f00000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000008514a940968a73f736e774d0000000000000004d1877a31a73c7cb31c02b9e7d7c336531562b21e00000000000000000000000000000000000000000000000000000000000000800000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f9960000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000004444c5dc75cb358380d2e3de08a900000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008514a940968a73f0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c59900000000000000000000000000000000000000000000000000000000000001f4000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000010009046d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c5998000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec70000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c599000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002000000000000000000000000003674ed9c52d903c6c3a468592ac27fe71b3cd849000000000000000000000000000000000000000000000000000000003b9aca000000000000000000000000000000000000000000000000000000000000181cd00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000000010000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f9960000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000003b9aca0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ad7b22536f75726365223a226b79626572737761702d74657374222c22416d6f756e74496e555344223a223939382e373638313539222c22416d6f756e744f7574555344223a223939382e363836343138222c22416d6f756e744f7574223a2231353936323033222c22526f7574654944223a2266653665646334385164424d667075533a396232623230336139794a4b56374157222c2254696d657374616d70223a313738323239333232377d00000000000000000000000000000000000000'; uint256 feeBefore; uint256 feeAfter; @@ -40,6 +39,10 @@ contract ConditionalSwapTest is BaseTest { // Derived swap ratio (amountOut_raw * 1e18 / amountIn_raw) for the mock prices: 1e15. uint256 internal constant ORACLE_RATIO = 1e15; uint256 internal constant PYTH_FEE = 0.001 ether; + uint256 internal constant PYTH_MAX_STALENESS = 30 minutes; + uint256 internal constant REAL_ORACLE_MAX_STALENESS = 15 hours; + bytes constant PYTH_UPDATE_DATA = + hex'504e41550100000003b801000000060d00461ea6caa392697ca4698dbf66f2f00f963c90b5b6db5255e4c85d4dafd6525f680a9abcde11b1d69904f282381b9bd544f88fca66e152589f0c5e9e54cd69180101db5c7584140358639ed9bba2055f9541d08656b01814143b0c8217ae347821f42b0c7685e6fba5d8dea408d72a5ff244e5fab1ccb6f30175c1c0e937618e8cb10102dd360df4be6c68dbe8169d73541384b37b7bba59e5697ab445109fecef919b175cad5cb5fca45255739733c8689fd28580ff30cf68d5e44f8c09457da5dd6ec8000498e4f639087d60bba599b5e105497c86d1771581b401ba0716afa587da23eab777dec74994fdaaa13e2968f0c51869c7f0ba9ace3980fd8e6427b881f0b5fce20105fdbd037cd2e6e28d61e9e2fd7d5be64613d8ff19a0eeafa6b498da3fa210a96a6e4d82f18d4267e006548aafdaa976eab7033ae5a8a3e0dae111d189fa9e9fe10006d654bed39cb4b75553339843a455b35ff0cbbc6fbdb3f55a1023e2a666a23fa75306d9e43d251fc9867ec0eac005a8e3f78f8ec154ae3b5a1c41f2bf78a920e2000802475dfe2082ffc96a6ad714af4384e131827d4192cc7ba2c3f35ccb516c6dfa488b018865c76d8164d971c13409b1bc790101cb780145ab78d8c146f56c46e3010a52f1bb05c9f90f0fb0b80a1e5af9db95e3059190b13f97022f79702618011abc7c5c9bc094a178e3832bdf1f00bad31e8d768ae6814c39c0bce9920f242f45f3000ba32159178fda2fc6d14a9a8616ed8a1feb117f9e22bbdfa86bc1474df78ea9ac23693e676ebef3849534c18bbef80ca488b7930fe550f48a016af34a3b9ddcc1010dfc1051da6e0b4aecbda8f8aac314aae5115b1bf91d09e71d856136f8ac0b48632cadc7a543b435db1e628314673de87e6b128600a580b9190dfdebbc5ba83efd001059b2ce802ee65bd3b1dec19f52a725ff380ce4af224ce27e0c5e8498bfeaae4b7aafb0fdae5949c564e0caab3513736dddcd80742e7ab8f7d2a6565cf7e1099900117b02be79bac0aedcbc82abd111d751b57bb4526afaff97a4c1aadc630bb21a6d09d73f5e7005b72ff6b14a2e8ffc5230eecc98563c8fd1b30b9622252fca2a9c0012d4c28041457a3a001964f75a177d0117c03fdb105a20e6a9e5d23c8f79fa7b5466639b439c2d6a066cfa9611c11c3d78ca1ae9523ea014e66f1a36366d4ef24d006a3b9bdb00000000001ae101faedac5851e32b9b23b5f9411a8c2bac4aae3ed4dd7b811dd1a72ea4aa71000000000cbe08440141555756000000000011cbc1ff00002710dd513a3b10d8385f8fb1b87d90c30741d517e390020055002b89b9dc8fdf9f34709a5b106b472f0f39bb6ca9ce04b0fd7f2e971688e2e53b0000000005f3c81800000000000167e5fffffff8000000006a3b9bdb000000006a3b9bda0000000005f3d446000000000000ed620d7ba6cde7d52e8aa397dc322ed599d8e73dbbe5a791736e606ac2aafad0e62c64c4eeb60baa6a2b77b455e47f0434bdcddc4f7d8a32f706e7fe149238c38ba67a96e5419eb9c4a56a7866d5b732ee190a6d4f34f6bb1a581269e7919a75197b2df72ec6dffeff4b6eeef97b49569e0a0225360f46eb321ff03096e28689aa132bcf4c95f161e8b668a43b96d05f9fdd8e60b22647db7f1cf1aa3f6d27fd88e2e72a8ad0ba703d94d6ec77ff5fa554b0abf001fb2932c0c1861802f1de7c46e3b4142c5117e3767fbbeb7a8d10399ef7be9f56d302ce4587c62650377101a56ad42c948257d9577e8ed671d2868fa7201df4baab12cecfcb177cb715d620e80e1f0a98b82e005500e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43000005b3f1391dd3000000009ff5722cfffffff8000000006a3b9bdb000000006a3b9bda000005b2daaa2020000000007e076ee80d91ce32105e3cf4383215149cb0515352a28d511addfafa2eea3b08c2a2826cf524dfe185ee61cd53211c4c590ec242e3294e3e9ba697465d6136fd5101a5ac8dceee2b5432ee486de3aaa3700717083fc700c0ce9f84f6348de4b9bcb2fd143be228834d6c3f5c4bb29e1a9c6b2a0e1b222640cf806d221a8a9a87ce117827f45067802ef061c81495c38b6531b6a445f29180a862e0f57e4cbf909f0fbd11c7db59a85d19cef5d2335e397aba5353d6b7c1e496dd3ba08ef9de4d68d6aedb691984467d2fc1206fef134c51f25e85603c7173e6a4dc9750a5e7600500851e116d09ffa2b8a40a48ceb3f976e86c9652f4baab12cecfcb177cb715d620e80e1f0a98b82e'; bytes32 internal constant USDT_ID = keccak256('USDT/USD'); bytes32 internal constant WBTC_ID = keccak256('WBTC/USD'); @@ -58,6 +61,10 @@ contract ConditionalSwapTest is BaseTest { MockChainlinkFeed internal feedOut; MockPyth internal pyth; + function _selectFork() public virtual override { + vm.createSelectFork('mainnet', 25_386_536); + } + function setUp() public virtual override { super.setUp(); @@ -67,20 +74,22 @@ contract ConditionalSwapTest is BaseTest { deal(tokenIn, mainAddress, 1e30); conditionalSwapHook = new KSConditionalSwapHook(routers); - // fund the hook so it can pay Pyth update fees out of its own balance - vm.deal(address(conditionalSwapHook), 1 ether); // mock oracles feedIn = new MockChainlinkFeed(8, 1e8); // USDT/USD = $1 feedOut = new MockChainlinkFeed(8, int256(100_000e8)); // WBTC/USD = $100k pyth = new MockPyth(PYTH_FEE); + pyth.setPrice(USDT_ID, int64(1e8), int32(-8), vm.getBlockTimestamp()); + pyth.setPrice(WBTC_ID, int64(int256(100_000e8)), int32(-8), vm.getBlockTimestamp()); + _updateForkPythPrices(); } function _emptyLeg() internal pure returns (OracleLib.TokenOracle memory) { - return OracleLib.TokenOracle(OracleLib.OracleType.NONE, address(0), bytes32(0), 0); + return + OracleLib.TokenOracle(OracleLib.OracleType.NONE, address(0), bytes32(0), toPackedU128(0, 0)); } - function _chainlinkLeg(address feed, uint256 priceLimits) + function _chainlinkLeg(address feed, PackedU128 priceLimits) internal pure returns (OracleLib.TokenOracle memory) @@ -88,7 +97,7 @@ contract ConditionalSwapTest is BaseTest { return OracleLib.TokenOracle(OracleLib.OracleType.CHAINLINK, feed, bytes32(0), priceLimits); } - function _pythLeg(address pyth_, bytes32 priceId, uint256 priceLimits) + function _pythLeg(address pyth_, bytes32 priceId, PackedU128 priceLimits) internal pure returns (OracleLib.TokenOracle memory) @@ -99,32 +108,34 @@ contract ConditionalSwapTest is BaseTest { function _config( OracleLib.TokenOracle memory oracleIn, OracleLib.TokenOracle memory oracleOut, - uint256 oracleParams + PackedU128 oracleParams ) internal pure returns (OracleLib.OracleConfig memory) { return OracleLib.OracleConfig(oracleIn, oracleOut, oracleParams); } function _noOracle() internal pure returns (OracleLib.OracleConfig memory) { - return _config(_emptyLeg(), _emptyLeg(), 0); + return _config(_emptyLeg(), _emptyLeg(), toPackedU128(0, 0)); } - /// @dev oracleParams packed: maxStaleness 128bits | maxDeviationBps 128bits. - function _params(uint256 maxStaleness, uint256 maxDeviationBps) internal pure returns (uint256) { - return (maxStaleness << 128) | maxDeviationBps; + /// @dev oracleParams packed: maxStaleness 128bits | maxDeviation 128bits, scaled by 1e18. + function _params(uint256 maxStaleness, uint256 maxDeviation) internal pure returns (PackedU128) { + return toPackedU128(maxStaleness, maxDeviation); } /// @dev USD price band [price*(1-bpsBelow), price*(1+bpsAbove)], packed min 128 | max 128. function _band(uint256 price, uint256 bpsBelow, uint256 bpsAbove) internal pure - returns (uint256) + returns (PackedU128) { uint256 lower = (price * (10_000 - bpsBelow)) / 10_000; uint256 upper = (price * (10_000 + bpsAbove)) / 10_000; - return (lower << 128) | upper; + return toPackedU128(lower, upper); } - uint256 internal constant FULL_BAND = (uint256(0) << 128) | type(uint128).max; + function _fullBand() internal pure returns (PackedU128) { + return toPackedU128(0, type(uint128).max); + } // --------------------------------------------------------------------------- // Non-oracle conditional swap tests @@ -172,9 +183,6 @@ contract ConditionalSwapTest is BaseTest { params.returnAmount = params.returnAmount - afterSwapFee; - (address caller, bytes memory dkSignature, bytes memory gdSignature) = - _getCallerAndSignatures(params.mode, intentData, actionData); - uint256[2] memory routerBefore = [tokenIn.balanceOf(address(router)), tokenOut.balanceOf(address(router))]; uint256[2] memory mainAddressBefore = @@ -182,8 +190,7 @@ contract ConditionalSwapTest is BaseTest { uint256[2] memory feeReceiversBefore = [tokenIn.balanceOf(partnerRecipient), tokenOut.balanceOf(partnerRecipient)]; - vm.startPrank(caller); - router.execute(intentData, dkSignature, guardian, gdSignature, actionData); + _executeSwap(params.mode, intentData, actionData); assertEq(tokenIn.balanceOf(address(router)), routerBefore[0]); assertEq(tokenOut.balanceOf(address(router)), routerBefore[1]); @@ -205,11 +212,7 @@ contract ConditionalSwapTest is BaseTest { ); vm.warp(vm.getBlockTimestamp() + 100); - (address caller, bytes memory dkSignature, bytes memory gdSignature) = - _getCallerAndSignatures(mode, intentData, actionData); - - vm.startPrank(caller); - router.execute(intentData, dkSignature, guardian, gdSignature, actionData); + _executeSwap(mode, intentData, actionData); } function test_DCASwap_TimeBased(uint256 mode) public { @@ -218,11 +221,11 @@ contract ConditionalSwapTest is BaseTest { KSConditionalSwapHook.SwapCondition[] memory condition = new KSConditionalSwapHook.SwapCondition[](3); condition[0] = - _timeCondition(((vm.getBlockTimestamp() - 100) << 128) | (vm.getBlockTimestamp() + 100)); + _timeCondition(toPackedU128(vm.getBlockTimestamp() - 100, vm.getBlockTimestamp() + 100)); condition[1] = - _timeCondition(((vm.getBlockTimestamp() + 500) << 128) | (vm.getBlockTimestamp() + 700)); + _timeCondition(toPackedU128(vm.getBlockTimestamp() + 500, vm.getBlockTimestamp() + 700)); condition[2] = - _timeCondition(((vm.getBlockTimestamp() + 1000) << 128) | (vm.getBlockTimestamp() + 1200)); + _timeCondition(toPackedU128(vm.getBlockTimestamp() + 1000, vm.getBlockTimestamp() + 1200)); IntentData memory intentData; { @@ -252,10 +255,10 @@ contract ConditionalSwapTest is BaseTest { new KSConditionalSwapHook.SwapCondition[](1); condition[0] = KSConditionalSwapHook.SwapCondition({ swapLimit: 4, - timeLimits: (0 << 128) | type(uint128).max, - amountInLimits: (swapAmount << 128) | swapAmount, - maxFees: (0 << 128) | type(uint128).max, - priceLimits: ((1_000_000_000_000 - 100) << 128) | (1_000_000_000_000 + 100), + timeLimits: toPackedU128(0, type(uint128).max), + amountInLimits: toPackedU128(swapAmount, swapAmount), + maxFees: toPackedU128(0, type(uint128).max), + priceLimits: toPackedU128(1_000_000_000_000 - 100, 1_000_000_000_000 + 100), oracle: _noOracle() }); @@ -281,7 +284,7 @@ contract ConditionalSwapTest is BaseTest { KSConditionalSwapHook.SwapCondition[] memory condition = new KSConditionalSwapHook.SwapCondition[](1); condition[0] = - _timeCondition(((vm.getBlockTimestamp() + 100) << 128) | (vm.getBlockTimestamp() + 1000)); + _timeCondition(toPackedU128(vm.getBlockTimestamp() + 100, vm.getBlockTimestamp() + 1000)); IntentData memory intentData = _getIntentData(0, type(uint128).max, condition); _setUpMainAddress(intentData, false); @@ -290,12 +293,7 @@ contract ConditionalSwapTest is BaseTest { intentData.tokenData, _adjustRecipient(feeAfter == 0 ? swapdata2 : swapdata), false ); - (address caller, bytes memory dkSignature, bytes memory gdSignature) = - _getCallerAndSignatures(mode, intentData, actionData); - - vm.startPrank(caller); - vm.expectRevert(KSConditionalSwapHook.InvalidSwap.selector); - router.execute(intentData, dkSignature, guardian, gdSignature, actionData); + _expectExecuteRevert(mode, intentData, actionData, KSConditionalSwapHook.InvalidSwap.selector); } function testRevert_InvalidPriceCondition(uint256 mode) public { @@ -304,10 +302,10 @@ contract ConditionalSwapTest is BaseTest { new KSConditionalSwapHook.SwapCondition[](1); condition[0] = KSConditionalSwapHook.SwapCondition({ swapLimit: 1, - timeLimits: ((vm.getBlockTimestamp() - 100) << 128) | (vm.getBlockTimestamp() + 100), - amountInLimits: (0 << 128) | type(uint128).max, - maxFees: (0 << 128) | type(uint128).max, - priceLimits: (uint256(type(uint128).max) << 128) | type(uint128).max, + timeLimits: toPackedU128(vm.getBlockTimestamp() - 100, vm.getBlockTimestamp() + 100), + amountInLimits: toPackedU128(0, type(uint128).max), + maxFees: toPackedU128(0, type(uint128).max), + priceLimits: toPackedU128(type(uint128).max, type(uint128).max), oracle: _noOracle() }); @@ -318,12 +316,7 @@ contract ConditionalSwapTest is BaseTest { intentData.tokenData, _adjustRecipient(feeAfter == 0 ? swapdata2 : swapdata), false ); - (address caller, bytes memory dkSignature, bytes memory gdSignature) = - _getCallerAndSignatures(mode, intentData, actionData); - - vm.startPrank(caller); - vm.expectRevert(KSConditionalSwapHook.InvalidSwap.selector); - router.execute(intentData, dkSignature, guardian, gdSignature, actionData); + _expectExecuteRevert(mode, intentData, actionData, KSConditionalSwapHook.InvalidSwap.selector); } function testRevert_ExceedSwapLimit(uint256 mode) public { @@ -345,16 +338,9 @@ contract ConditionalSwapTest is BaseTest { bytes32 hash = router.hashTypedIntentData(intentData); assertEq(conditionalSwapHook.getSwapExecutionCount(hash, 0, 0), 0); - (address caller, bytes memory dkSignature, bytes memory gdSignature) = - _getCallerAndSignatures(mode, intentData, actionData); - - vm.startPrank(caller); - router.execute(intentData, dkSignature, guardian, gdSignature, actionData); + _executeSwap(mode, intentData, actionData); actionData.nonce += 1; - (caller, dkSignature, gdSignature) = _getCallerAndSignatures(mode, intentData, actionData); - vm.startPrank(caller); - vm.expectRevert(KSConditionalSwapHook.InvalidSwap.selector); - router.execute(intentData, dkSignature, guardian, gdSignature, actionData); + _expectExecuteRevert(mode, intentData, actionData, KSConditionalSwapHook.InvalidSwap.selector); assertEq(conditionalSwapHook.getSwapExecutionCount(hash, 0, 0), 1); } @@ -372,16 +358,14 @@ contract ConditionalSwapTest is BaseTest { ); actionData.erc20Ids[0] = 0; - (address caller, bytes memory dkSignature, bytes memory gdSignature) = - _getCallerAndSignatures(mode, intentData, actionData); - - vm.startPrank(caller); - vm.expectRevert( + _expectExecuteRevert( + mode, + intentData, + actionData, abi.encodeWithSelector( KSConditionalSwapHook.InvalidTokenIn.selector, makeAddr('dummy'), tokenIn ) ); - router.execute(intentData, dkSignature, guardian, gdSignature, actionData); } function testRevert_AmountInTooSmallOrTooLarge(uint256 mode, uint128 min, uint128 max) public { @@ -395,12 +379,7 @@ contract ConditionalSwapTest is BaseTest { intentData.tokenData, _adjustRecipient(feeAfter == 0 ? swapdata2 : swapdata), false ); - (address caller, bytes memory dkSignature, bytes memory gdSignature) = - _getCallerAndSignatures(mode, intentData, actionData); - - vm.startPrank(caller); - vm.expectRevert(KSConditionalSwapHook.InvalidSwap.selector); - router.execute(intentData, dkSignature, guardian, gdSignature, actionData); + _expectExecuteRevert(mode, intentData, actionData, KSConditionalSwapHook.InvalidSwap.selector); } function testRevert_ExceedFeeLimit(uint256 mode) public { @@ -420,12 +399,7 @@ contract ConditionalSwapTest is BaseTest { true ); - (address caller, bytes memory dkSignature, bytes memory gdSignature) = - _getCallerAndSignatures(mode, intentData, actionData); - - vm.startPrank(caller); - vm.expectRevert(KSConditionalSwapHook.InvalidSwap.selector); - router.execute(intentData, dkSignature, guardian, gdSignature, actionData); + _expectExecuteRevert(mode, intentData, actionData, KSConditionalSwapHook.InvalidSwap.selector); } function test_Chainlink_MarketTrigger_Pass(uint256 mode) public { @@ -433,9 +407,9 @@ contract ConditionalSwapTest is BaseTest { OracleLib.OracleConfig memory cfg = _config( _chainlinkLeg(address(feedIn), _band(USDT_USD, 100, 100)), _chainlinkLeg(address(feedOut), _band(BTC_USD, 100, 100)), - 0 + _params(0, 0) ); - _expectSwapOk(mode, cfg, _amountOutFor(ORACLE_RATIO), 0, new bytes[](0)); + _expectSwapOk(mode, cfg, _amountOutFor(ORACLE_RATIO)); } function test_Chainlink_MarketTrigger_Revert(uint256 mode) public { @@ -443,41 +417,55 @@ contract ConditionalSwapTest is BaseTest { // tokenOut band sits entirely above the live BTC price -> never met OracleLib.OracleConfig memory cfg = _config( _chainlinkLeg(address(feedIn), _band(USDT_USD, 100, 100)), - _chainlinkLeg(address(feedOut), (uint256(BTC_USD * 2) << 128) | type(uint128).max), - 0 + _chainlinkLeg(address(feedOut), toPackedU128(BTC_USD * 2, type(uint128).max)), + _params(0, 0) ); - _expectSwapRevert(mode, cfg, _amountOutFor(ORACLE_RATIO), 0, new bytes[](0)); + _expectSwapRevert(mode, cfg, _amountOutFor(ORACLE_RATIO)); } function test_Chainlink_SlippageGuard_Pass(uint256 mode) public { mode = bound(mode, 0, 2); OracleLib.OracleConfig memory cfg = _config( - _chainlinkLeg(address(feedIn), FULL_BAND), - _chainlinkLeg(address(feedOut), FULL_BAND), - _params(0, 1000) // 10% tolerance + _chainlinkLeg(address(feedIn), _fullBand()), + _chainlinkLeg(address(feedOut), _fullBand()), + _params(0, 1e17) // 10% tolerance + ); + _expectSwapOk(mode, cfg, _amountOutFor((ORACLE_RATIO * 105) / 100)); // +5% + } + + function test_Chainlink_SlippageGuard_MinRevert(uint256 mode) public { + mode = bound(mode, 0, 2); + OracleLib.OracleConfig memory cfg = _config( + _chainlinkLeg(address(feedIn), _fullBand()), + _chainlinkLeg(address(feedOut), _fullBand()), + _params(0, 1e16) // 1% tolerance ); - _expectSwapOk(mode, cfg, _amountOutFor((ORACLE_RATIO * 105) / 100), 0, new bytes[](0)); // +5% + _expectSwapRevert(mode, cfg, _amountOutFor((ORACLE_RATIO * 95) / 100)); // -5% } - function test_Chainlink_SlippageGuard_Revert(uint256 mode) public { + function testRevert_Chainlink_SlippageGuard_InvalidMaxDeviation(uint256 mode) public { mode = bound(mode, 0, 2); OracleLib.OracleConfig memory cfg = _config( - _chainlinkLeg(address(feedIn), FULL_BAND), - _chainlinkLeg(address(feedOut), FULL_BAND), - _params(0, 100) // 1% tolerance + _chainlinkLeg(address(feedIn), _fullBand()), + _chainlinkLeg(address(feedOut), _fullBand()), + _params(0, 1e18 + 1) ); - _expectSwapRevert(mode, cfg, _amountOutFor((ORACLE_RATIO * 105) / 100), 0, new bytes[](0)); // +5% + + (IntentData memory intentData, ActionData memory actionData) = + _buildIntentAndAction(_single(cfg), _amountOutFor(ORACLE_RATIO)); + _expectExecuteRevert(mode, intentData, actionData, OracleLib.InvalidMaxDeviation.selector); } function test_Chainlink_SingleLeg_Out(uint256 mode) public { mode = bound(mode, 0, 2); // only the output token is constrained; the input leg is left unconfigured - OracleLib.OracleConfig memory cfg = - _config(_emptyLeg(), _chainlinkLeg(address(feedOut), _band(BTC_USD, 100, 100)), 0); - _expectSwapOk(mode, cfg, _amountOutFor(ORACLE_RATIO), 0, new bytes[](0)); + OracleLib.OracleConfig memory cfg = _config( + _emptyLeg(), _chainlinkLeg(address(feedOut), _band(BTC_USD, 100, 100)), _params(0, 0) + ); + _expectSwapOk(mode, cfg, _amountOutFor(ORACLE_RATIO)); } - function test_Pyth_Update_And_Validate(uint256 mode) public { + function test_Pyth_Validate(uint256 mode) public { mode = bound(mode, 0, 2); OracleLib.OracleConfig memory cfg = _config( _pythLeg(address(pyth), USDT_ID, _band(USDT_USD, 100, 100)), @@ -485,33 +473,28 @@ contract ConditionalSwapTest is BaseTest { _params(3600, 0) ); - uint256 hookBalBefore = address(conditionalSwapHook).balance; - (IntentData memory intentData, ActionData memory actionData) = _buildIntentAndAction( - _single(cfg), _amountOutFor(ORACLE_RATIO), 0, _pythUpdateData(vm.getBlockTimestamp()) - ); + (IntentData memory intentData, ActionData memory actionData) = + _buildIntentAndAction(_single(cfg), _amountOutFor(ORACLE_RATIO)); uint256 balBefore = IERC20(tokenOut).balanceOf(mainAddress); _executeSwap(mode, intentData, actionData); assertGt(IERC20(tokenOut).balanceOf(mainAddress), balBefore); - assertEq(pyth.updateCount(), 1); // hook pushed exactly one update - assertEq(address(pyth).balance, PYTH_FEE); // fee delivered to Pyth - assertEq(address(conditionalSwapHook).balance, hookBalBefore - PYTH_FEE); // paid by the hook + assertEq(pyth.updateCount(), 0); } function test_Pyth_StalePrice_Revert(uint256 mode) public { mode = bound(mode, 0, 2); OracleLib.OracleConfig memory cfg = _config( - _pythLeg(address(pyth), USDT_ID, FULL_BAND), - _pythLeg(address(pyth), WBTC_ID, FULL_BAND), + _pythLeg(address(pyth), USDT_ID, _fullBand()), + _pythLeg(address(pyth), WBTC_ID, _fullBand()), _params(100, 0) // 100s staleness bound ); - // publish time well in the past -> getPriceNoOlderThan reverts during afterExecution + // seeded price is too old by the time getPriceNoOlderThan is called in afterExecution vm.warp(vm.getBlockTimestamp() + 1000); - bytes[] memory updateData = _pythUpdateData(vm.getBlockTimestamp() - 1000); (IntentData memory intentData, ActionData memory actionData) = - _buildIntentAndAction(_single(cfg), _amountOutFor(ORACLE_RATIO), 0, updateData); + _buildIntentAndAction(_single(cfg), _amountOutFor(ORACLE_RATIO)); (address caller, bytes memory dk, bytes memory gd) = _getCallerAndSignatures(mode, intentData, actionData); @@ -530,8 +513,8 @@ contract ConditionalSwapTest is BaseTest { conditions[0] = _oracleCondition( _config( _chainlinkLeg(address(feedIn), _band(USDT_USD, 100, 100)), - _chainlinkLeg(address(feedOut), (uint256(BTC_USD * 2) << 128) | type(uint128).max), - 0 + _chainlinkLeg(address(feedOut), toPackedU128(BTC_USD * 2, type(uint128).max)), + _params(0, 0) ) ); // condition 1: Pyth, bands bracket the live prices -> matches @@ -543,10 +526,8 @@ contract ConditionalSwapTest is BaseTest { ) ); - // oracleUpdateIndex = 1 -> refresh uses condition 1's (Pyth) config - (IntentData memory intentData, ActionData memory actionData) = _buildIntentAndAction( - conditions, _amountOutFor(ORACLE_RATIO), 1, _pythUpdateData(vm.getBlockTimestamp()) - ); + (IntentData memory intentData, ActionData memory actionData) = + _buildIntentAndAction(conditions, _amountOutFor(ORACLE_RATIO)); bytes32 hash = router.hashTypedIntentData(intentData); uint256 balBefore = IERC20(tokenOut).balanceOf(mainAddress); @@ -560,54 +541,39 @@ contract ConditionalSwapTest is BaseTest { function test_Fork_ChainlinkReal_MarketTrigger_Pass(uint256 mode) public { mode = bound(mode, 0, 2); (uint256 priceIn, uint256 priceOut, uint256 ratio) = - _readReal(_realChainlink(FULL_BAND, FULL_BAND)); + _readReal(_realChainlink(_fullBand(), _fullBand())); OracleLib.OracleConfig memory cfg = _realChainlink(_band(priceIn, 100, 100), _band(priceOut, 100, 100)); - _expectSwapOk(mode, cfg, _amountOutFor(ratio), 0, new bytes[](0)); + _expectSwapOk(mode, cfg, _amountOutFor(ratio)); } function test_Fork_ChainlinkReal_MarketTrigger_Revert(uint256 mode) public { mode = bound(mode, 0, 2); - (, uint256 priceOut, uint256 ratio) = _readReal(_realChainlink(FULL_BAND, FULL_BAND)); + (, uint256 priceOut, uint256 ratio) = _readReal(_realChainlink(_fullBand(), _fullBand())); OracleLib.OracleConfig memory cfg = - _realChainlink(FULL_BAND, (uint256(priceOut * 2) << 128) | type(uint128).max); - _expectSwapRevert(mode, cfg, _amountOutFor(ratio), 0, new bytes[](0)); + _realChainlink(_fullBand(), toPackedU128(priceOut * 2, type(uint128).max)); + _expectSwapRevert(mode, cfg, _amountOutFor(ratio)); } function test_Fork_ChainlinkReal_SlippageGuard_Revert(uint256 mode) public { mode = bound(mode, 0, 2); - (,, uint256 ratio) = _readReal(_realChainlink(FULL_BAND, FULL_BAND)); + (,, uint256 ratio) = _readReal(_realChainlink(_fullBand(), _fullBand())); - OracleLib.OracleConfig memory cfg = _realChainlink(FULL_BAND, FULL_BAND); - cfg.oracleParams = _params(0, 200); // 2% tolerance - _expectSwapRevert(mode, cfg, _amountOutFor((ratio * 110) / 100), 0, new bytes[](0)); // +10% + OracleLib.OracleConfig memory cfg = _realChainlink(_fullBand(), _fullBand()); + cfg.oracleParams = _params(REAL_ORACLE_MAX_STALENESS, 2e16); // 2% tolerance + _expectSwapRevert(mode, cfg, _amountOutFor((ratio * 90) / 100)); // -10% } function test_Fork_PythReal_Read_Pass(uint256 mode) public { mode = bound(mode, 0, 2); - (uint256 priceIn, uint256 priceOut, uint256 ratio) = _readReal(_realPyth(FULL_BAND, FULL_BAND)); + (uint256 priceIn, uint256 priceOut, uint256 ratio) = + _readReal(_realPyth(_fullBand(), _fullBand())); OracleLib.OracleConfig memory cfg = _realPyth(_band(priceIn, 200, 200), _band(priceOut, 200, 200)); - _expectSwapOk(mode, cfg, _amountOutFor(ratio), 0, new bytes[](0)); - } - - function test_Fork_PythReal_Update_Pass() public { - bytes[] memory updateData = new bytes[](1); - updateData[0] = pythUpdateData; - - (uint256 priceIn, uint256 priceOut, uint256 ratio) = _readReal(_realPyth(FULL_BAND, FULL_BAND)); - OracleLib.OracleConfig memory cfg = - _realPyth(_band(priceIn, 200, 200), _band(priceOut, 200, 200)); - - (IntentData memory intentData, ActionData memory actionData) = - _buildIntentAndAction(_single(cfg), _amountOutFor(ratio), 0, updateData); - - uint256 balBefore = IERC20(tokenOut).balanceOf(mainAddress); - _executeSwap(1, intentData, actionData); - assertGt(IERC20(tokenOut).balanceOf(mainAddress), balBefore); + _expectSwapOk(mode, cfg, _amountOutFor(ratio)); } function test_Fork_RealSwap_ChainlinkChainlink_Pass(uint256 mode) public { @@ -644,37 +610,45 @@ contract ConditionalSwapTest is BaseTest { function _runRealSwapOracle(uint256 mode, bool inPyth, bool outPyth, bool ok) internal { mode = bound(mode, 0, 2); + (IntentData memory intentData, ActionData memory actionData) = + _buildRealSwapOracle(inPyth, outPyth, ok); + _assertRealSwapOracle(mode, intentData, actionData, ok); + } + function _buildRealSwapOracle(bool inPyth, bool outPyth, bool ok) + internal + returns (IntentData memory intentData, ActionData memory actionData) + { // read the live per-leg USD prices (read-only; Pyth legs are already on-chain at the fork) - (uint256 priceIn, uint256 priceOut,) = - _readReal(_config(_legIn(inPyth, FULL_BAND), _legOut(outPyth, FULL_BAND), 0)); + (uint256 priceIn, uint256 priceOut,) = _readReal( + _config(_legIn(inPyth, _fullBand()), _legOut(outPyth, _fullBand()), _realOracleParams(0)) + ); - uint256 bandOut = - ok ? _band(priceOut, 100, 100) : (uint256(priceOut * 2) << 128) | type(uint128).max; + PackedU128 bandOut = + ok ? _band(priceOut, 100, 100) : toPackedU128(priceOut * 2, type(uint128).max); OracleLib.OracleConfig memory cfg = _config( _legIn(inPyth, _band(priceIn, 100, 100)), _legOut(outPyth, bandOut), - _params(0, 1000) // 10% slippage tolerance (live price vs the captured route) + _realOracleParams(1e17) // 10% slippage tolerance (live price vs the captured route) ); - bytes[] memory updateData = new bytes[](0); - if (inPyth || outPyth) { - updateData = new bytes[](1); - updateData[0] = pythUpdateData; - } - KSConditionalSwapHook.SwapCondition[] memory condition = _single(cfg); - IntentData memory intentData = _getIntentData(0, type(uint128).max, condition); + intentData = _getIntentData(0, type(uint128).max, condition); _setUpMainAddress(intentData, false); - ActionData memory actionData = _getActionData( + actionData = _getActionData( intentData.tokenData, _adjustRecipient(feeAfter == 0 ? swapdata2 : swapdata), false ); - // oracleUpdateIndex = 0 (single condition); supply the real Pyth blob when any leg is Pyth - actionData.hookActionData = - abi.encode(uint256(0), (feeBefore << 128) | feeAfter, uint256(0), updateData); + actionData.hookActionData = abi.encode(uint256(0), (feeBefore << 128) | feeAfter); + } + function _assertRealSwapOracle( + uint256 mode, + IntentData memory intentData, + ActionData memory actionData, + bool ok + ) internal { (address caller, bytes memory dk, bytes memory gd) = _getCallerAndSignatures(mode, intentData, actionData); @@ -692,20 +666,30 @@ contract ConditionalSwapTest is BaseTest { assertGt(IERC20(tokenOut).balanceOf(mainAddress), balBefore); } - function _legIn(bool pyth_, uint256 band) internal pure returns (OracleLib.TokenOracle memory) { - return - pyth_ ? _pythLeg(PYTH_MAINNET, PYTH_USDT_USD, band) : _chainlinkLeg(CHAINLINK_USDT_USD, band); + function _legIn(bool pyth_, PackedU128 band) + internal + pure + returns (OracleLib.TokenOracle memory) + { + return pyth_ + ? _pythLeg(PYTH_MAINNET, PYTH_USDT_USD, band) + : _chainlinkLeg(CHAINLINK_USDT_USD, band); } - function _legOut(bool pyth_, uint256 band) internal pure returns (OracleLib.TokenOracle memory) { - return - pyth_ ? _pythLeg(PYTH_MAINNET, PYTH_BTC_USD, band) : _chainlinkLeg(CHAINLINK_WBTC_USD, band); + function _legOut(bool pyth_, PackedU128 band) + internal + pure + returns (OracleLib.TokenOracle memory) + { + return pyth_ + ? _pythLeg(PYTH_MAINNET, PYTH_BTC_USD, band) + : _chainlinkLeg(CHAINLINK_WBTC_USD, band); } /// @dev Chainlink and Pyth agree on the live BTC price within 1%. function test_Fork_RealOracles_Agree() public view { - (,, uint256 clRatio) = _readReal(_realChainlink(FULL_BAND, FULL_BAND)); - (,, uint256 pythRatio) = _readReal(_realPyth(FULL_BAND, FULL_BAND)); + (,, uint256 clRatio) = _readReal(_realChainlink(_fullBand(), _fullBand())); + (,, uint256 pythRatio) = _readReal(_realPyth(_fullBand(), _fullBand())); uint256 diff = clRatio > pythRatio ? clRatio - pythRatio : pythRatio - clRatio; assertLt(diff * 10_000, clRatio * 100); } @@ -714,17 +698,19 @@ contract ConditionalSwapTest is BaseTest { // Helpers // --------------------------------------------------------------------------- - function _realChainlink(uint256 bandIn, uint256 bandOut) + function _realChainlink(PackedU128 bandIn, PackedU128 bandOut) internal pure returns (OracleLib.OracleConfig memory) { return _config( - _chainlinkLeg(CHAINLINK_USDT_USD, bandIn), _chainlinkLeg(CHAINLINK_WBTC_USD, bandOut), 0 + _chainlinkLeg(CHAINLINK_USDT_USD, bandIn), + _chainlinkLeg(CHAINLINK_WBTC_USD, bandOut), + _params(REAL_ORACLE_MAX_STALENESS, 0) ); } - function _realPyth(uint256 bandIn, uint256 bandOut) + function _realPyth(PackedU128 bandIn, PackedU128 bandOut) internal pure returns (OracleLib.OracleConfig memory) @@ -732,10 +718,22 @@ contract ConditionalSwapTest is BaseTest { return _config( _pythLeg(PYTH_MAINNET, PYTH_USDT_USD, bandIn), _pythLeg(PYTH_MAINNET, PYTH_BTC_USD, bandOut), - 0 + _params(PYTH_MAX_STALENESS, 0) ); } + function _realOracleParams(uint256 maxDeviation) internal pure returns (PackedU128) { + return _params(REAL_ORACLE_MAX_STALENESS, maxDeviation); + } + + function _updateForkPythPrices() internal { + bytes[] memory updateData = new bytes[](1); + updateData[0] = PYTH_UPDATE_DATA; + uint256 fee = IPyth(PYTH_MAINNET).getUpdateFee(updateData); + vm.deal(address(this), address(this).balance + fee); + IPyth(PYTH_MAINNET).updatePriceFeeds{value: fee}(updateData); + } + function _readReal(OracleLib.OracleConfig memory cfg) internal view @@ -751,10 +749,10 @@ contract ConditionalSwapTest is BaseTest { { return KSConditionalSwapHook.SwapCondition({ swapLimit: 4, - timeLimits: (0 << 128) | type(uint128).max, - amountInLimits: (0 << 128) | type(uint128).max, - maxFees: (0 << 128) | type(uint128).max, - priceLimits: (0 << 128) | type(uint128).max, + timeLimits: toPackedU128(0, type(uint128).max), + amountInLimits: toPackedU128(0, type(uint128).max), + maxFees: toPackedU128(0, type(uint128).max), + priceLimits: toPackedU128(0, type(uint128).max), oracle: oracle }); } @@ -768,7 +766,7 @@ contract ConditionalSwapTest is BaseTest { conditions[0] = _oracleCondition(oracle); } - function _timeCondition(uint256 timeLimits) + function _timeCondition(PackedU128 timeLimits) internal view returns (KSConditionalSwapHook.SwapCondition memory) @@ -776,9 +774,9 @@ contract ConditionalSwapTest is BaseTest { return KSConditionalSwapHook.SwapCondition({ swapLimit: 1, timeLimits: timeLimits, - amountInLimits: (swapAmount << 128) | swapAmount, - maxFees: (0 << 128) | type(uint128).max, - priceLimits: (0 << 128) | type(uint128).max, + amountInLimits: toPackedU128(swapAmount, swapAmount), + maxFees: toPackedU128(0, type(uint128).max), + priceLimits: toPackedU128(0, type(uint128).max), oracle: _noOracle() }); } @@ -788,12 +786,6 @@ contract ConditionalSwapTest is BaseTest { return (realizedPrice * swapAmount) / 1e18; } - function _pythUpdateData(uint256 publishTime) internal pure returns (bytes[] memory updateData) { - updateData = new bytes[](2); - updateData[0] = abi.encode(USDT_ID, int64(1e8), int32(-8), publishTime); - updateData[1] = abi.encode(WBTC_ID, int64(int256(100_000e8)), int32(-8), publishTime); - } - function _mockSwapAction() internal view returns (ActionData memory actionData) { TokenData memory tokenData; tokenData.erc20Data = new ERC20Data[](1); @@ -814,10 +806,16 @@ contract ConditionalSwapTest is BaseTest { function _buildIntentAndAction( KSConditionalSwapHook.SwapCondition[] memory conditions, - uint256 amountOut, - uint256 oracleUpdateIndex, - bytes[] memory updateData + uint256 amountOut ) internal returns (IntentData memory intentData, ActionData memory actionData) { + intentData = _buildIntent(conditions); + actionData = _buildMockSwapAction(amountOut); + } + + function _buildIntent(KSConditionalSwapHook.SwapCondition[] memory conditions) + internal + returns (IntentData memory intentData) + { { uint256 tmp = swapAmount; swapAmount = type(uint256).max; @@ -825,7 +823,13 @@ contract ConditionalSwapTest is BaseTest { _setUpMainAddress(intentData, false); swapAmount = tmp; } + } + function _buildMockSwapAction(uint256 amountOut) + internal + view + returns (ActionData memory actionData) + { TokenData memory tokenData; tokenData.erc20Data = new ERC20Data[](1); tokenData.erc20Data[0] = ERC20Data({token: tokenIn, amount: swapAmount, permitData: ''}); @@ -835,32 +839,24 @@ contract ConditionalSwapTest is BaseTest { abi.encode(tokenIn, tokenOut, swapAmount, amountOut, mainAddress, mainAddress), true ); - actionData.hookActionData = abi.encode(uint256(0), uint256(0), oracleUpdateIndex, updateData); + actionData.hookActionData = abi.encode(uint256(0), uint256(0)); } - function _expectSwapOk( - uint256 mode, - OracleLib.OracleConfig memory cfg, - uint256 amountOut, - uint256 oracleUpdateIndex, - bytes[] memory updateData - ) internal { + function _expectSwapOk(uint256 mode, OracleLib.OracleConfig memory cfg, uint256 amountOut) + internal + { (IntentData memory intentData, ActionData memory actionData) = - _buildIntentAndAction(_single(cfg), amountOut, oracleUpdateIndex, updateData); + _buildIntentAndAction(_single(cfg), amountOut); uint256 balBefore = IERC20(tokenOut).balanceOf(mainAddress); _executeSwap(mode, intentData, actionData); assertGt(IERC20(tokenOut).balanceOf(mainAddress), balBefore); } - function _expectSwapRevert( - uint256 mode, - OracleLib.OracleConfig memory cfg, - uint256 amountOut, - uint256 oracleUpdateIndex, - bytes[] memory updateData - ) internal { + function _expectSwapRevert(uint256 mode, OracleLib.OracleConfig memory cfg, uint256 amountOut) + internal + { (IntentData memory intentData, ActionData memory actionData) = - _buildIntentAndAction(_single(cfg), amountOut, oracleUpdateIndex, updateData); + _buildIntentAndAction(_single(cfg), amountOut); (address caller, bytes memory dk, bytes memory gd) = _getCallerAndSignatures(mode, intentData, actionData); vm.startPrank(caller); @@ -878,6 +874,34 @@ contract ConditionalSwapTest is BaseTest { vm.stopPrank(); } + function _expectExecuteRevert( + uint256 mode, + IntentData memory intentData, + ActionData memory actionData, + bytes4 selector + ) internal { + (address caller, bytes memory dkSignature, bytes memory gdSignature) = + _getCallerAndSignatures(mode, intentData, actionData); + vm.startPrank(caller); + vm.expectRevert(selector); + router.execute(intentData, dkSignature, guardian, gdSignature, actionData); + vm.stopPrank(); + } + + function _expectExecuteRevert( + uint256 mode, + IntentData memory intentData, + ActionData memory actionData, + bytes memory revertData + ) internal { + (address caller, bytes memory dkSignature, bytes memory gdSignature) = + _getCallerAndSignatures(mode, intentData, actionData); + vm.startPrank(caller); + vm.expectRevert(revertData); + router.execute(intentData, dkSignature, guardian, gdSignature, actionData); + vm.stopPrank(); + } + function _swap( uint256 mode, IntentData memory intentData, @@ -935,9 +959,7 @@ contract ConditionalSwapTest is BaseTest { ) : actionCalldata) : actionCalldata, - hookActionData: abi.encode( - uint256(0), (feeBefore << 128) | feeAfter, uint256(0), new bytes[](0) - ), + hookActionData: abi.encode(uint256(0), (feeBefore << 128) | feeAfter), extraData: '', deadline: vm.getBlockTimestamp() + 1 days, nonce: 0 @@ -961,10 +983,10 @@ contract ConditionalSwapTest is BaseTest { hookData.swapConditions[0] = new KSConditionalSwapHook.SwapCondition[](1); hookData.swapConditions[0][0] = KSConditionalSwapHook.SwapCondition({ swapLimit: 1, - timeLimits: (vm.getBlockTimestamp() << 128) | (vm.getBlockTimestamp() + 1 days), - amountInLimits: (min << 128) | max, - maxFees: (maxSrcFee << 128) | maxDstFee, - priceLimits: (0 << 128) | type(uint128).max, + timeLimits: toPackedU128(vm.getBlockTimestamp(), vm.getBlockTimestamp() + 1 days), + amountInLimits: toPackedU128(min, max), + maxFees: toPackedU128(maxSrcFee, maxDstFee), + priceLimits: toPackedU128(0, type(uint128).max), oracle: _noOracle() }); }