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
38 changes: 38 additions & 0 deletions src/adapters/liquidity-party/IPartyPool.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

/// @title IPartyPool
/// @notice Minimal interface for a Liquidity Party (LMSR) PartyPool — the swap entry point only.
/// @dev The signature and selector (0x4ae93e1d) match the deployed mainnet pool
/// (see ../lmsr-amm/src/IPartyPool.sol). Note this differs from an earlier revision of the
/// protocol that used `int128 limitPrice` in place of `uint256 minAmountOut`; the deployed
/// pools we integrate use `minAmountOut`. The fee is charged on the OUTPUT side, so
/// `maxAmountIn` is the exact gross input transferred (nothing is added for fees).
interface IPartyPool {
/// @param payer account that pays the input; for PREFUNDING it MUST equal msg.sender
/// (the pool enforces `require(msg.sender == payer, "prefunding: caller != payer")`).
/// @param fundingSelector Funding.PREFUNDING (0x00000001): input already transferred to the pool.
/// @param receiver address that receives the net output tokens.
/// @param inputTokenIndex index of the input asset in the pool's token list.
/// @param outputTokenIndex index of the output asset in the pool's token list.
/// @param maxAmountIn exact input to consume (fee is on the output side, not added to input).
/// @param minAmountOut minimum net output; reverts "slippage control" if not met. Pass 0 to disable.
/// @param deadline timestamp after which the call reverts; pass 0 to ignore.
/// @param unwrap if true, native-wrapper output is unwrapped to native currency.
/// @param cbData callback data for callback-style funding selectors (empty for PREFUNDING).
/// @return amountIn actual input consumed.
/// @return amountOut net output sent to receiver (gross output minus outFee).
/// @return outFee fee taken from the gross output.
function swap(
address payer,
bytes4 fundingSelector,
address receiver,
uint256 inputTokenIndex,
uint256 outputTokenIndex,
uint256 maxAmountIn,
uint256 minAmountOut,
uint256 deadline,
bool unwrap,
bytes memory cbData
) external payable returns (uint256 amountIn, uint256 amountOut, uint256 outFee);
}
81 changes: 81 additions & 0 deletions src/adapters/liquidity-party/LiquidityPartyAdapter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import './IPartyPool.sol';

import '../../libraries/CalldataDecoder.sol';
import '../../libraries/TokenHelper.sol';

/// @title LiquidityPartyAdapter
/// @notice KyberSwap adapter for Liquidity Party ("LiqP") LMSR multi-asset pools.
/// @dev A LiqP pool holds `n` tokens and quotes any ordered pair `(i -> j)` off a shared LMSR
/// `q`-vector. The router selects the single pair to swap; the off-chain dex-lib simulator
/// emits the `(indexIn, indexOut)` indices via GetMetaInfo, which the (off-repo) KyberSwap
/// calldata encoder abi-encodes into `data`. We use word-aligned `abi.encode` + CalldataDecoder
/// to match the other adapters (uniswap-v2/v3, wasabi) — the house convention the encoder emits.
///
/// Funding uses PREFUNDING (0x00000001): the input token is transferred into the pool and
/// `swap()` is called atomically in the same tx. PREFUNDING funds from an unauthenticated
/// balance delta, so it is only safe when the transfer and the swap are atomic — which they
/// are here. The pool requires `msg.sender == payer` on the PREFUNDING path, so `payer` is
/// this adapter. The fee is charged on the OUTPUT side, so the full `amountIn` is consumed
/// (no fee-on-transfer / rebasing tokens: admin-created pools reject them).
contract LiquidityPartyAdapter {
using TokenHelper for address;
using CalldataDecoder for bytes;

/// @notice Funding.PREFUNDING selector — input already sent to the pool before swap().
/// @dev Must match Funding.PREFUNDING in ../lmsr-amm/src/Funding.sol.
bytes4 internal constant FUNDING_PREFUNDING = 0x00000001;

/// @param data abi.encode(address pool, uint256 indexIn, uint256 indexOut) (3 words).
/// @param amountIn input already prefunded to this adapter by the executor.
/// @param tokenIn input token address (LiqP token at `indexIn`).
/// @param recipient address that receives the output tokens.
/// @dev The 4th arg (tokenOut) is unused: swap() returns the net output directly. It is kept in
/// the signature for the uniform execute<Dex>(bytes, uint256, address, address, address) ABI.
function executeLiquidityParty(
bytes calldata data,
uint256 amountIn,
address tokenIn,
address,
address recipient
) external payable returns (uint256 amountUnused, uint256 amountOut) {
(address pool, uint256 indexIn, uint256 indexOut) = _decodeData(data);

// push the prefunded input into the pool, then swap atomically (PREFUNDING requires atomicity).
// swap() returns the net output (gross minus outFee) sent to the recipient, so use it directly.
// LiqP rejects fee-on-transfer / rebasing tokens (pool-side balance checks), so this equals the
// recipient balance diff to the wei — no extra balanceOf needed (asserted in the fork test).
tokenIn.safeTransfer(pool, amountIn);
(, amountOut,) = IPartyPool(pool)
.swap(
address(this), // payer == msg.sender, required by the PREFUNDING path
FUNDING_PREFUNDING,
recipient, // deliver net output directly to the recipient
indexIn,
indexOut,
amountIn,
0, // minAmountOut: the router enforces slippage
0, // deadline: the router enforces deadlines
false, // unwrap: KyberSwap handles native (un)wrapping, so treat LiqP as ERC-only
'' // no callback data for PREFUNDING
);

// LiqP consumes exactly maxAmountIn; there is no fee-on-transfer, so nothing is left unused.
// amountUnused = 0; // Save gas by relying on the implicit initialization to zero
}

/// @dev Word-aligned layout matching the other adapters: abi.encode(pool, indexIn, indexOut).
/// The off-repo KyberSwap encoder must emit exactly this (same shape as uniswap-v2's
/// abi.encode(pool, fee, feeDenom)).
function _decodeData(bytes calldata data)
internal
pure
returns (address pool, uint256 indexIn, uint256 indexOut)
{
pool = data.decodeAddress(0);
indexIn = data.decodeUint256(1);
indexOut = data.decodeUint256(2);
}
}
115 changes: 115 additions & 0 deletions test/adapters/liquidity-party/LiquidityPartyAdapter.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import 'forge-std/Test.sol';

import 'src/adapters/liquidity-party/LiquidityPartyAdapter.sol';

/// @notice PartyInfo view helper used to cross-check the executed swap output to the wei.
interface IPartyInfo {
function swapAmounts(address pool, uint256 i, uint256 j, uint256 maxAmountIn)
external
view
returns (uint256 amountIn, uint256 amountOut, uint256 outFee);
}

/// @dev Exposes the internal packed-calldata decoder for unit testing.
contract LiquidityPartyAdapterExposed is LiquidityPartyAdapter {
function decodeData(bytes calldata data)
external
pure
returns (address pool, uint256 indexIn, uint256 indexOut)
{
return _decodeData(data);
}
}

contract LiquidityPartyAdapterTest is Test {
using TokenHelper for address;

LiquidityPartyAdapterExposed adapter;

// Mainnet Liquidity Party deployment (chainId 1), see ../lmsr-amm/deployment/liqp-deployments.json.
address constant PARTY_INFO = 0xefF3Ed388D3887e7C9F375B7f1ad8A0B77C05643;
// Live 3-token test pool: [USDC(0), WETH(1), AAVE(2)].
address constant POOL = 0x1270Da05Cf1d047763CEEfDe25a4a5438b26fdA6;

address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; // index 0, 6 decimals
address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; // index 1, 18 decimals
address constant AAVE = 0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9; // index 2, 18 decimals

uint256 constant USDC_INDEX = 0;
uint256 constant WETH_INDEX = 1;
uint256 constant AAVE_INDEX = 2;

address[3] tokens = [USDC, WETH, AAVE];

address recipient = makeAddr('recipient');

// Fixed fork for a known pool state (matches the dex-lib golden tests). Override RPC via RPC_1.
string RPC_URL = vm.envOr('RPC_1', string('https://1rpc.io/eth'));
uint256 constant BLOCK_NUMBER = 25_301_966;

function setUp() public {
vm.createSelectFork(RPC_URL, BLOCK_NUMBER);
adapter = new LiquidityPartyAdapterExposed();
}

function testDecodeData() public view {
address mockPool = 0x1234567890123456789012345678901234567890;
bytes memory data = abi.encode(mockPool, uint256(3), uint256(7));

(address pool, uint256 indexIn, uint256 indexOut) = adapter.decodeData(data);

assertEq(pool, mockPool, 'pool');
assertEq(indexIn, 3, 'indexIn');
assertEq(indexOut, 7, 'indexOut');
assertEq(data.length, 96, 'abi.encode(pool, i, j) is 3 words');
}

/// @dev Runs one swap through the adapter and asserts the executed output matches
/// PartyInfo.swapAmounts to the wei, plus the KyberSwap two-value return contract.
function _runAndCheck(uint256 i, uint256 j, uint256 amountIn) internal {
address tokenIn = tokens[i];
address tokenOut = tokens[j];

(, uint256 expectedOut,) = IPartyInfo(PARTY_INFO).swapAmounts(POOL, i, j, amountIn);
assertGt(expectedOut, 0, 'expected output should be positive');

// prefund the adapter (the executor does this in production), never the pool
deal(tokenIn, address(adapter), amountIn);

bytes memory data = abi.encode(POOL, i, j);

uint256 balOutBefore = tokenOut.balanceOf(recipient);
(uint256 amountUnused, uint256 amountOut) =
adapter.executeLiquidityParty(data, amountIn, tokenIn, tokenOut, recipient);

assertEq(amountUnused, 0, 'no unused input');
assertEq(amountOut, expectedOut, 'amountOut matches PartyInfo.swapAmounts to the wei');
assertEq(
tokenOut.balanceOf(recipient) - balOutBefore, amountOut, 'recipient received amountOut'
);
assertEq(tokenIn.balanceOf(address(adapter)), 0, 'input fully consumed');
}

function test_swap_USDC_to_WETH() public {
_runAndCheck(USDC_INDEX, WETH_INDEX, 300_000); // 0.3 USDC
}

function test_swap_WETH_to_USDC() public {
_runAndCheck(WETH_INDEX, USDC_INDEX, 200_000_000_000_000); // 0.0002 WETH
}

function test_swap_USDC_to_AAVE() public {
_runAndCheck(USDC_INDEX, AAVE_INDEX, 300_000); // 0.3 USDC
}

function test_swap_AAVE_to_WETH() public {
_runAndCheck(AAVE_INDEX, WETH_INDEX, 5_000_000_000_000_000); // 0.005 AAVE
}

function test_swap_AAVE_to_USDC() public {
_runAndCheck(AAVE_INDEX, USDC_INDEX, 5_000_000_000_000_000); // 0.005 AAVE
}
}
Loading