Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/solidity-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ jobs:
strategy:
fail-fast: false
matrix:
profile: [balancer, fluid, lido, mellow, safe-guard]
profile: [balancer, fluid, kyber, lido, mellow, safe-guard]
Comment thread
makemake-kbo marked this conversation as resolved.
steps:
- name: Checkout
uses: actions/checkout@v6
Expand Down
35 changes: 24 additions & 11 deletions examples/kyber/README.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,39 @@
# KyberSwap examples

This example targets MetaAggregationRouterV2. Kyber has two deployed runtime families at the shared
router address, so deployments must explicitly select whether the original `swapGeneric` surface
is present.
This example targets MetaAggregationRouterV2. Kyber uses the same router address across chains but
has distinct verified runtime families, so the example exposes separate assertion artifacts:

- `KyberOriginalMetaAggregationRouterAssertion` includes `swapGeneric` and the original
partial-fill semantics.
- `KyberModernMetaAggregationRouterAssertion` omits `swapGeneric` and never interprets caller bit
zero as a partial-fill bypass.

The family is fixed by the selected bytecode artifact; there is no deployer-supplied family
boolean.

```sh
FOUNDRY_PROFILE=kyber forge build
FOUNDRY_PROFILE=kyber pcl test
```

Only `assertReceiverGetsMinReturn` is registered. It checks standard ERC-20, non-partial output by
comparing the declared receiver's balance before and after the exact swap call. The live router
already performs the same balance-delta check, so this is a narrow redundant postcondition. Native
output and original-family partial fills remain outside it. The original-family partial-fill flag
is not applied to modern deployments.
Only `assertReceiverGetsMinReturn` is registered. It compares the declared receiver's balance
before and after the exact swap call. The live router already performs a receiver-delta check, so
this is a narrow redundant postcondition for a buggy or compromised settlement path, not a
universal protocol invariant.

The additive outer-call model excludes native output, same-token routes, and rebasing/reflection
tokens. On the original family, a partial-fill route is skipped only when native input or
`_SHOULD_CLAIM` can make the router recompute `spentAmount` below `desc.amount`; caller-controlled
bit zero alone does not disable the flat check. The modern artifact never applies partial-fill
semantics.

`assertNoThirdPartyAllowanceDrain` is retained as an unarmed diagnostic. A `Transfer` event does
not identify which spender used the allowance, so a route where an executor spends its own tokens
can be rejected merely because it has a stale router allowance. Approval events do not establish
causality either. The old approval behavior tests are therefore no longer part of the executable
semantic suite.

The configured router is bound to the adopter, but deployment operations must still pin the chain
and verified runtime family. The local route mocks are unit fixtures and do not reproduce every
live command encoding.
The configured router is bound to the adopter. Deployment operations must still pin the chain and
verified runtime family before choosing an artifact. The local model reproduces the accounting and
`swap` dispatch boundaries used by the semantic tests. Its `swapSimpleMode` cases are isolated
calldata/assertion-branch fixtures, not a reproduction of Kyber's full `SimpleSwapData` engine.
123 changes: 88 additions & 35 deletions examples/kyber/src/KyberMetaAggregationRouterAssertion.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,19 @@ import {PhEvm} from "credible-std/PhEvm.sol";
import {KyberMetaAggregationRouterHelpers} from "./KyberMetaAggregationRouterHelpers.sol";
import {IKyberMetaAggregationRouterV2Like, SwapDescriptionV2} from "./KyberMetaAggregationRouterInterfaces.sol";

/// @title KyberMetaAggregationRouterAssertion
/// @title KyberMetaAggregationRouterAssertionBase
/// @author Phylax Systems
/// @notice Recipient-credit postcondition for KyberSwap MetaAggregationRouterV2.
/// @dev Transfer and Approval logs do not identify the spender that moved funds, so the former
/// allowance-drain heuristic is intentionally not registered. `originalRouterFamily_` must be
/// true only for deployments whose bit 0 means partial fill and which expose `swapGeneric`.
contract KyberMetaAggregationRouterAssertion is KyberMetaAggregationRouterHelpers {
constructor(address router_, bool originalRouterFamily_)
KyberMetaAggregationRouterHelpers(router_, originalRouterFamily_)
{}

/// @notice Registers the protected MetaAggregationRouterV2 settlement entry points.
/// @dev Both checks are call-scoped so reads are bound to the exact triggered settlement.
function triggers() external view override {
registerFnCallTrigger(
this.assertReceiverGetsMinReturn.selector, IKyberMetaAggregationRouterV2Like.swap.selector
);
if (ORIGINAL_ROUTER_FAMILY) {
registerFnCallTrigger(
this.assertReceiverGetsMinReturn.selector, IKyberMetaAggregationRouterV2Like.swapGeneric.selector
);
/// @notice Shared distinct-token ERC20 recipient-credit postcondition for KyberSwap routers.
/// @dev This is deliberately a narrow defense-in-depth call postcondition, not a universal
/// protocol invariant. Same-token, native-output, rebasing/reflection, and genuinely
/// pro-rated partial-fill routes are outside its additive balance-delta model.
abstract contract KyberMetaAggregationRouterAssertionBase is KyberMetaAggregationRouterHelpers {
constructor(address router_) KyberMetaAggregationRouterHelpers(router_) {}

function _registerTriggers(bytes4[] memory selectors) internal view {
for (uint256 i; i < selectors.length; ++i) {
registerFnCallTrigger(this.assertReceiverGetsMinReturn.selector, selectors[i]);
}
registerFnCallTrigger(
this.assertReceiverGetsMinReturn.selector, IKyberMetaAggregationRouterV2Like.swapSimpleMode.selector
);
}

/// @notice Legacy diagnostic retained for source compatibility; it is not registered.
Expand All @@ -44,19 +32,18 @@ contract KyberMetaAggregationRouterAssertion is KyberMetaAggregationRouterHelper
_assertOnlyInitiatorAllowanceExercised(ctx.callStart, initiator);
}

/// @notice The declared recipient must be credited at least the signed `minReturnAmount`.
/// @dev Decodes the swap description from the triggered calldata and compares the recipient's
/// buy-token balance across the call as a fork-aware delta. This is path-independent
/// defense-in-depth: the live router already enforces min-return against this same
/// recipient balance delta, so the check earns its keep only if that guard is ever
/// bypassed (a buggy/compromised settlement path), while still pinning the user-signed
/// minimum from outside the router's own accounting.
/// @notice Checks a distinct, non-rebasing ERC20 receiver gets the applicable flat minimum.
/// @dev The call-scoped check intentionally duplicates Kyber's receiver balance-delta guard as
/// defense in depth for a buggy or compromised settlement path. It skips accounting
/// domains where the outer-call delta is not equivalent to Kyber's internal window.
///
/// Out of scope, and skipped to avoid false positives:
/// - zero-minimum swaps (nothing to enforce);
/// - native-asset payouts (`dstToken == ETH_SENTINEL`) — this surface reads ERC20 balances only;
/// - partial-fill orders (`_PARTIAL_FILL` flag) — the router enforces a pro-rated minimum
/// keyed to the actual spent amount, which a flat `minReturnAmount` floor would not match.
/// - native-asset payouts (`dstToken == ETH_SENTINEL`);
/// - same-token routes, because Kyber snapshots after the source debit while this assertion
/// snapshots around the outer call;
/// - original-family partial fills only when the selected entry point makes the live router
/// compute `spentAmount` from a balance delta rather than fixing it to `desc.amount`.
/// An unset recipient (`dstReceiver == address(0)`) is NOT skipped: the router credits
/// `msg.sender` in that case, so the check is retargeted to the resolved initiator.
function assertReceiverGetsMinReturn() external view {
Expand All @@ -65,8 +52,8 @@ contract KyberMetaAggregationRouterAssertion is KyberMetaAggregationRouterHelper

SwapDescriptionV2 memory desc = _swapDescriptionFor(ctx.selector, ph.callinputAt(ctx.callStart));
if (
desc.minReturnAmount == 0 || desc.dstToken == ETH_SENTINEL
|| (ORIGINAL_ROUTER_FAMILY && _flagsChecked(desc.flags, PARTIAL_FILL))
desc.minReturnAmount == 0 || desc.dstToken == ETH_SENTINEL || desc.srcToken == desc.dstToken
|| _usesProRatedMinimum(ctx.selector, desc)
) {
return;
}
Expand All @@ -87,4 +74,70 @@ contract KyberMetaAggregationRouterAssertion is KyberMetaAggregationRouterHelper
afterBalance - beforeBalance >= desc.minReturnAmount, "Kyber: dstReceiver credited below minReturnAmount"
);
}

/// @dev Modern-family deployments override this with `false`: bit zero is otherwise ignored.
function _usesProRatedMinimum(bytes4 selector, SwapDescriptionV2 memory desc) internal pure virtual returns (bool);
}

/// @title KyberOriginalMetaAggregationRouterAssertion
/// @notice Assertion artifact for the verified original router family with `swapGeneric`.
/// @dev The artifact fixes original-family semantics at compile time; deployers cannot select a
/// caller-controlled-bit interpretation with a constructor boolean.
contract KyberOriginalMetaAggregationRouterAssertion is KyberMetaAggregationRouterAssertionBase {
constructor(address router_) KyberMetaAggregationRouterAssertionBase(router_) {}

/// @notice Registers all verified original-family settlement entry points.
function triggers() external view override {
_registerTriggers(protectedSelectors());
}

/// @notice Returns the selector manifest consumed directly by `triggers()`.
function protectedSelectors() public pure returns (bytes4[] memory selectors) {
selectors = new bytes4[](3);
selectors[0] = IKyberMetaAggregationRouterV2Like.swap.selector;
selectors[1] = IKyberMetaAggregationRouterV2Like.swapGeneric.selector;
selectors[2] = IKyberMetaAggregationRouterV2Like.swapSimpleMode.selector;
}

function _usesProRatedMinimum(bytes4 selector, SwapDescriptionV2 memory desc)
internal
pure
override
returns (bool)
{
if (!_flagsChecked(desc.flags, PARTIAL_FILL)) {
return false;
}
if (selector == IKyberMetaAggregationRouterV2Like.swapSimpleMode.selector) {
return true;
}
if (selector == IKyberMetaAggregationRouterV2Like.swap.selector) {
return desc.srcToken == ETH_SENTINEL || _flagsChecked(desc.flags, SIMPLE_SWAP);
}
return selector == IKyberMetaAggregationRouterV2Like.swapGeneric.selector
&& (desc.srcToken == ETH_SENTINEL || _flagsChecked(desc.flags, SHOULD_CLAIM));
Comment thread
makemake-kbo marked this conversation as resolved.
}
}

/// @title KyberModernMetaAggregationRouterAssertion
/// @notice Assertion artifact for the verified modern router family without `swapGeneric`.
/// @dev Modern-family bit zero has no partial-fill meaning, so it never disables the flat check.
contract KyberModernMetaAggregationRouterAssertion is KyberMetaAggregationRouterAssertionBase {
constructor(address router_) KyberMetaAggregationRouterAssertionBase(router_) {}

/// @notice Registers only the verified modern-family settlement entry points.
function triggers() external view override {
_registerTriggers(protectedSelectors());
}

/// @notice Returns the selector manifest consumed directly by `triggers()`.
function protectedSelectors() public pure returns (bytes4[] memory selectors) {
selectors = new bytes4[](2);
selectors[0] = IKyberMetaAggregationRouterV2Like.swap.selector;
selectors[1] = IKyberMetaAggregationRouterV2Like.swapSimpleMode.selector;
}

function _usesProRatedMinimum(bytes4, SwapDescriptionV2 memory) internal pure override returns (bool) {
return false;
}
}
20 changes: 9 additions & 11 deletions examples/kyber/src/KyberMetaAggregationRouterHelpers.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,21 @@ abstract contract KyberMetaAggregationRouterHelpers is Assertion {
bytes32 internal constant ERC20_TRANSFER_SIG = keccak256("Transfer(address,address,uint256)");
bytes32 internal constant ERC20_APPROVAL_SIG = keccak256("Approval(address,address,uint256)");

/// @notice `desc.flags` bit that marks an order the router may fill only partially.
/// @dev Mirrors `MetaAggregationRouterV2._PARTIAL_FILL`. When set, the router enforces a
/// pro-rated minimum (`returnAmount * amount >= minReturnAmount * spentAmount`) it
/// measures from the actual spent amount, so a flat `minReturnAmount` floor does not hold.
/// @notice `desc.flags` bit that marks an order the original router may fill partially.
uint256 internal constant PARTIAL_FILL = 0x01;

/// @notice `desc.flags` bit that makes the original router collect source funds first.
uint256 internal constant SHOULD_CLAIM = 0x04;

/// @notice `desc.flags` bit that dispatches `swap` through `swapSimpleMode`.
uint256 internal constant SIMPLE_SWAP = 0x20;

/// @notice The single fixed-address MetaAggregationRouterV2 this assertion protects.
address internal immutable ROUTER;
bool internal immutable ORIGINAL_ROUTER_FAMILY;

constructor(address router_, bool originalRouterFamily_) {
constructor(address router_) {
require(router_ != address(0), "Kyber: router zero");
ROUTER = router_;
ORIGINAL_ROUTER_FAMILY = originalRouterFamily_;
registerAssertionSpec(AssertionSpec.Reshiram);
}

Expand Down Expand Up @@ -158,10 +159,7 @@ abstract contract KyberMetaAggregationRouterHelpers is Assertion {

uint256 standingAllowance = _allowanceAt(transfers[i].emitter, from, ROUTER, beforeFork);
bool grantedInCall = _approvedRouterInCall(approvals, transfers[i].emitter, from);
require(
standingAllowance == 0 && !grantedInCall,
"Kyber: swap exercised third-party router allowance"
);
require(standingAllowance == 0 && !grantedInCall, "Kyber: swap exercised third-party router allowance");
}
}

Expand Down
9 changes: 9 additions & 0 deletions examples/kyber/src/KyberMetaAggregationRouterInterfaces.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ struct SwapExecutionParams {
bytes clientData;
}

/// @notice Route data decoded by the original router's `swapSimpleMode` sequence engine.
struct SimpleSwapData {
address[] firstPools;
uint256[] firstSwapAmounts;
bytes[] swapDatas;
uint256 deadline;
bytes destTokenFeeData;
}

/// @notice Public KyberSwap MetaAggregationRouterV2 settlement entry points protected here.
/// @dev All three fund-moving settlement entry points are modeled, each verified against the
/// mainnet `MetaAggregationRouterV2` source (`0x6131B5fae19EA4f9D964eAc0408E4408b66337b5`):
Expand Down
Loading