Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion remappings.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
forge-std/=lib/ks-common-sc/lib/forge-std/src/
ks-common-sc/=lib/ks-common-sc
ks-common-sc/=lib/ks-common-sc
openzeppelin-contracts=lib/ks-common-sc/lib/openzeppelin-contracts
137 changes: 70 additions & 67 deletions src/hooks/swap/KSConditionalSwapHook.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@
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 {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';

contract KSConditionalSwapHook is BaseStatefulHook {
using TokenHelper for address;
using CalldataDecoder for bytes;

error InvalidTokenIn(address tokenIn, address actualTokenIn);
error AmountInMismatch(uint256 amountIn, uint256 actualAmountIn);
Expand Down Expand Up @@ -38,18 +41,20 @@ 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;
uint256 timeLimits;
uint256 amountInLimits;
uint256 maxFees;
uint256 priceLimits;
PackedU128 timeLimits;
PackedU128 amountInLimits;
PackedU128 maxFees;
PackedU128 priceLimits;
OracleLib.OracleConfig oracle;
}

struct SwapValidationData {
SwapCondition[] swapConditions;
bytes32 intentHash;
uint256 intentIndex;
address tokenIn;
Expand All @@ -75,6 +80,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());
Expand All @@ -88,42 +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) =
_decodeAndValidateHookActionData(actionData.hookActionData, swapHookData);
_decodeHookActionData(actionData.hookActionData);

address tokenIn = intentData.tokenData.erc20Data[actionData.erc20Ids[0]].token;
address tokenOut = swapHookData.dstTokens[index];
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];

require(
tokenIn == swapHookData.srcTokens[index],
InvalidTokenIn(tokenIn, swapHookData.srcTokens[index])
);

fees = new uint256[](1);
fees[0] = (amountIn * intentSrcFee) / PRECISION;
beforeExecutionData = abi.encode(
SwapValidationData({
swapConditions: swapHookData.swapConditions[index],
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);
Expand Down Expand Up @@ -161,13 +170,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) {
Expand Down Expand Up @@ -212,29 +225,34 @@ 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];

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;
}

if (!OracleLib.validate(condition.oracle, tokenIn, tokenOut, price)) {
continue;
}

Expand Down Expand Up @@ -287,17 +305,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
Expand All @@ -309,20 +316,16 @@ 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).
function _decodeHookActionData(bytes calldata data)
internal
view
pure
returns (uint256 index, uint256 intentSrcFee, uint256 intentDstFee)
{
uint256 packedFees;
assembly ('memory-safe') {
index := calldataload(data.offset)
packedFees := calldataload(add(data.offset, 0x20))
}

intentSrcFee = packedFees >> 128;
intentDstFee = uint128(packedFees);
index = data.decodeUint256(0);
(uint128 srcFee, uint128 dstFee) = PackedU128.wrap(data.decodeUint256(1)).unpack();
intentSrcFee = srcFee;
intentDstFee = dstFee;
}

// @dev: equivalent to abi.decode(data, (SwapValidationData))
Expand All @@ -332,7 +335,7 @@ contract KSConditionalSwapHook is BaseStatefulHook {
returns (SwapValidationData calldata validationData)
{
assembly ('memory-safe') {
validationData := add(data.offset, calldataload(data.offset))
validationData := data.offset
}
}
}
17 changes: 17 additions & 0 deletions src/interfaces/oracle/external/AggregatorV3Interface.sol
Original file line number Diff line number Diff line change
@@ -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
);
}
17 changes: 17 additions & 0 deletions src/interfaces/oracle/external/IPyth.sol
Original file line number Diff line number Diff line change
@@ -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);
}
Loading
Loading