Skip to content

Commit d9a7197

Browse files
committed
feat: add SmarDex exact-in swap helper
1 parent 63f2261 commit d9a7197

3 files changed

Lines changed: 126 additions & 0 deletions

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.28;
3+
4+
import { SmardexSwapHelpers } from "../src/helpers/SmardexSwapHelpers.sol";
5+
import { Script } from "forge-std/Script.sol";
6+
7+
contract SmardexSwapHelpersDeployer is Script {
8+
function run() public returns (SmardexSwapHelpers smardexSwapHelpers) {
9+
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
10+
11+
vm.startBroadcast(deployerPrivateKey);
12+
13+
// Stateless pure encoder: router-agnostic, same address (CREATE2) on every chain.
14+
smardexSwapHelpers = new SmardexSwapHelpers{ salt: "SmardexSwapHelpers" }();
15+
16+
vm.stopBroadcast();
17+
}
18+
}

src/helpers/SmardexSwapHelpers.sol

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.28;
3+
4+
import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol";
5+
6+
/// @notice Weiroll-friendly encoder for SmarDex UniPool exact-in swaps.
7+
/// @dev The SmarDex router's `execute(commands, inputs, deadline)` carries `amountIn` ABI-encoded
8+
/// inside the opaque `inputs[0]` blob, so the Weiroll VM cannot substitute a reference/placeholder
9+
/// amountIn there. This helper takes the swap params as plain (Weiroll-substitutable) arguments and
10+
/// returns the encoded `inputs` array, which the standard feeds straight into `router.execute(...)`.
11+
/// It never holds funds, pulls tokens, or approves: the wallet approves the router directly and the
12+
/// router pulls `tokenIn` from the wallet (payment = TransferFrom). Mirrors BalancerHelpers.
13+
contract SmardexSwapHelpers {
14+
using SafeCast for uint256;
15+
16+
/// @dev SmarDex router payment mode: pull tokenIn from the execute caller via transferFrom.
17+
uint8 private constant PAYMENT_TRANSFER_FROM = 2;
18+
19+
/// @notice Build the `inputs` array for a SmarDex UniPool exact-in single swap.
20+
/// @param tokenIn ERC20 sold.
21+
/// @param tokenOut ERC20 bought.
22+
/// @param amountIn Exact input amount (Weiroll-substitutable; cast to uint128, reverts on overflow).
23+
/// @param minAmountOut Slippage floor enforced by the router (cast to uint128).
24+
/// @param to Recipient of the output (the executing wallet).
25+
/// @return inputs The single-element `bytes[]` passed to `ISmardexRouter.execute`.
26+
function encodeSwapInputs(
27+
address tokenIn,
28+
address tokenOut,
29+
uint256 amountIn,
30+
uint256 minAmountOut,
31+
address to
32+
)
33+
external
34+
pure
35+
returns (bytes[] memory inputs)
36+
{
37+
address[] memory path = new address[](2);
38+
path[0] = tokenIn;
39+
path[1] = tokenOut;
40+
41+
inputs = new bytes[](1);
42+
inputs[0] = abi.encode(amountIn.toUint128(), minAmountOut.toUint128(), path, to, PAYMENT_TRANSFER_FROM);
43+
}
44+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.28;
3+
4+
import { SmardexSwapHelpers } from "../../../../src/helpers/SmardexSwapHelpers.sol";
5+
import { Test } from "forge-std/Test.sol";
6+
7+
contract SmardexSwapHelpersTest is Test {
8+
SmardexSwapHelpers public helpers;
9+
10+
uint8 internal constant PAYMENT_TRANSFER_FROM = 2;
11+
12+
address internal constant TOKEN_IN = 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0; // wstETH
13+
address internal constant TOKEN_OUT = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; // WETH
14+
address internal constant TO = 0x00000000000000000000000000000000DeaDBeef;
15+
16+
function setUp() public {
17+
helpers = new SmardexSwapHelpers();
18+
}
19+
20+
function test_encodeSwapInputs_matchesRouterEncoding() public view {
21+
uint256 amountIn = 1 ether;
22+
uint256 minAmountOut = 0.9 ether;
23+
24+
bytes[] memory inputs = helpers.encodeSwapInputs(TOKEN_IN, TOKEN_OUT, amountIn, minAmountOut, TO);
25+
26+
assertEq(inputs.length, 1, "single input");
27+
28+
address[] memory path = new address[](2);
29+
path[0] = TOKEN_IN;
30+
path[1] = TOKEN_OUT;
31+
bytes memory expected = abi.encode(uint128(amountIn), uint128(minAmountOut), path, TO, PAYMENT_TRANSFER_FROM);
32+
assertEq(inputs[0], expected, "encoding must match the SmarDex swapInput layout");
33+
}
34+
35+
function test_encodeSwapInputs_decodesRoundTrip() public view {
36+
uint256 amountIn = 12_345;
37+
uint256 minAmountOut = 678;
38+
39+
bytes[] memory inputs = helpers.encodeSwapInputs(TOKEN_IN, TOKEN_OUT, amountIn, minAmountOut, TO);
40+
41+
(uint128 dAmountIn, uint128 dMinAmountOut, address[] memory dPath, address dTo, uint8 dPayment) =
42+
abi.decode(inputs[0], (uint128, uint128, address[], address, uint8));
43+
44+
assertEq(dAmountIn, uint128(amountIn), "amountIn");
45+
assertEq(dMinAmountOut, uint128(minAmountOut), "minAmountOut");
46+
assertEq(dPath.length, 2, "path length");
47+
assertEq(dPath[0], TOKEN_IN, "path[0]");
48+
assertEq(dPath[1], TOKEN_OUT, "path[1]");
49+
assertEq(dTo, TO, "to");
50+
assertEq(dPayment, PAYMENT_TRANSFER_FROM, "payment");
51+
}
52+
53+
function test_encodeSwapInputs_revertsOnAmountInOverflow() public {
54+
uint256 tooBig = uint256(type(uint128).max) + 1;
55+
vm.expectRevert();
56+
helpers.encodeSwapInputs(TOKEN_IN, TOKEN_OUT, tooBig, 0, TO);
57+
}
58+
59+
function test_encodeSwapInputs_revertsOnMinAmountOutOverflow() public {
60+
uint256 tooBig = uint256(type(uint128).max) + 1;
61+
vm.expectRevert();
62+
helpers.encodeSwapInputs(TOKEN_IN, TOKEN_OUT, 1 ether, tooBig, TO);
63+
}
64+
}

0 commit comments

Comments
 (0)