Skip to content
Merged

Hanji #459

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ead8b87
WIP: Hanji (many TODOs)
duncancmt Jan 2, 2026
0334275
WIP: resolve TODOs
duncancmt Jan 13, 2026
5f5137c
`forge fmt`
duncancmt Jan 13, 2026
3fab3a5
Merge branch 'dcmt/osaka-bugs' into dcmt/hanji
duncancmt Jan 13, 2026
b363586
Bug! Wrong `order_owner`
duncancmt Jan 13, 2026
d7b388a
Compilation errors
duncancmt Jan 13, 2026
5a78c45
Plumbing
duncancmt Jan 13, 2026
3598177
`forge fmt`
duncancmt Jan 13, 2026
88226f8
Plumb Hanji on Monad
duncancmt Jan 13, 2026
74a2fe2
`CHANGELOG.md`
duncancmt Jan 13, 2026
2a973aa
Add Hanji integration test for Monad
duncancmt Jan 13, 2026
881c120
Refactor Hanji integration tests per review feedback
duncancmt Jan 13, 2026
d3768a3
DRY up Hanji integration tests
duncancmt Jan 13, 2026
a939de6
Use specific TooMuchSlippage error in Hanji slippage test
duncancmt Jan 13, 2026
4ebdcb8
Bug! Wrong scaling factor for native sells to Hanji
duncancmt Jan 13, 2026
12435a8
Merge branch 'llm/hanji' into dcmt/hanji
duncancmt Jan 13, 2026
2984d23
Be stricter about handling of wraps/unwraps
duncancmt Jan 13, 2026
464a3a9
Typo
duncancmt Jan 13, 2026
83b8e63
Add slippage and settlerAsOrderOwner tests for USDC->WMON direction
duncancmt Jan 13, 2026
001144b
Merge branch 'master' into dcmt/hanji
duncancmt Jan 13, 2026
2d0aaec
Typo
duncancmt Jan 13, 2026
17be2ea
WIP: go back to the non-`Proxy` version of the Hanji ABI because that…
duncancmt Jan 14, 2026
5f0996b
`forge fmt`
duncancmt Jan 14, 2026
486d376
Remove tests not supported by new (old) Hanji ABI
duncancmt Jan 14, 2026
7fa070d
Fixing tests
duncancmt Jan 14, 2026
b8209a9
Fixing tests -- cannot buy WMON, only MON
duncancmt Jan 14, 2026
8e78d25
Finish fixing Hanji tests -- Hanji does not support custody optimization
duncancmt Jan 14, 2026
e4145a1
Update snaps
duncancmt Jan 14, 2026
b11987c
Add explanation of Hanji limitation to `CHANGELOG.md`
duncancmt Jan 15, 2026
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

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ Master list of UniV3 forks:

### Non-breaking changes

* Add `HANJI` action for Hanji order book liquidity source on Base and Monad
* Note that while it is possible to sell eith ETH (native) or WETH (wrapped
native) to Hanji pools with the wrapped native asset as one of the tokens,
attempting to buy WETH (wrapped native) is not possible. You will always get
raw ETH (native).

## 2025-12-29

### Breaking changes
Expand Down
11 changes: 11 additions & 0 deletions src/ISettlerActions.sol
Original file line number Diff line number Diff line change
Expand Up @@ -322,4 +322,15 @@ interface ISettlerActions {
BebopMakerSignature memory makerSignature,
uint256 amountOutMin
) external;

function HANJI(
address sellToken,
uint256 bps,
address pool,
uint256 sellScalingFactor,
uint256 buyScalingFactor,
bool isAsk,
uint256 priceLimit,
uint256 minBuyAmount
) external;
}
17 changes: 16 additions & 1 deletion src/chains/Base/Common.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {BalancerV3} from "../../core/BalancerV3.sol";
import {PancakeInfinity} from "../../core/PancakeInfinity.sol";
import {Renegade, BASE_SELECTOR} from "../../core/Renegade.sol";
import {Bebop} from "../../core/Bebop.sol";
import {Hanji} from "../../core/Hanji.sol";

import {IMsgSender} from "../../interfaces/IMsgSender.sol";
import {FreeMemory} from "../../utils/FreeMemory.sol";
Expand Down Expand Up @@ -70,7 +71,8 @@ abstract contract BaseMixin is
PancakeInfinity,
//EulerSwap,
Renegade,
Bebop
Bebop,
Hanji
{
using FastLogic for bool;

Expand Down Expand Up @@ -166,6 +168,19 @@ abstract contract BaseMixin is
(address target, IERC20 baseToken, bytes memory renegadeData) = abi.decode(data, (address, IERC20, bytes));

sellToRenegade(target, baseToken, renegadeData);
} else if (action == uint32(ISettlerActions.HANJI.selector)) {
(
IERC20 sellToken,
uint256 bps,
address pool,
uint256 sellScalingFactor,
uint256 buyScalingFactor,
bool isAsk,
uint256 priceLimit,
uint256 minBuyAmount
) = abi.decode(data, (IERC20, uint256, address, uint256, uint256, bool, uint256, uint256));

sellToHanji(sellToken, bps, pool, sellScalingFactor, buyScalingFactor, isAsk, priceLimit, minBuyAmount);
} else {
return false;
}
Expand Down
16 changes: 15 additions & 1 deletion src/chains/Monad/Common.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {SettlerBase} from "../../SettlerBase.sol";
import {IERC20} from "@forge-std/interfaces/IERC20.sol";
import {UniswapV4} from "../../core/UniswapV4.sol";
import {BalancerV3} from "../../core/BalancerV3.sol";
import {Hanji} from "../../core/Hanji.sol";
import {LfjTokenMill} from "../../core/LfjTokenMill.sol";
import {FreeMemory} from "../../utils/FreeMemory.sol";

Expand Down Expand Up @@ -33,7 +34,7 @@ import {MONAD_POOL_MANAGER} from "../../core/UniswapV4Addresses.sol";
import {SettlerAbstract} from "../../SettlerAbstract.sol";
import {Permit2PaymentAbstract} from "../../core/Permit2PaymentAbstract.sol";

abstract contract MonadMixin is FreeMemory, SettlerBase, BalancerV3, UniswapV4, LfjTokenMill {
abstract contract MonadMixin is FreeMemory, SettlerBase, BalancerV3, UniswapV4, Hanji, LfjTokenMill {
constructor() {
assert(block.chainid == 143 || block.chainid == 31337);
}
Expand Down Expand Up @@ -73,6 +74,19 @@ abstract contract MonadMixin is FreeMemory, SettlerBase, BalancerV3, UniswapV4,
) = abi.decode(data, (address, IERC20, uint256, bool, uint256, uint256, bytes, uint256));

sellToBalancerV3(recipient, sellToken, bps, feeOnTransfer, hashMul, hashMod, fills, amountOutMin);
} else if (action == uint32(ISettlerActions.HANJI.selector)) {
(
IERC20 sellToken,
uint256 bps,
address pool,
uint256 sellScalingFactor,
uint256 buyScalingFactor,
bool isAsk,
uint256 priceLimit,
uint256 minBuyAmount
) = abi.decode(data, (IERC20, uint256, address, uint256, uint256, bool, uint256, uint256));

sellToHanji(sellToken, bps, pool, sellScalingFactor, buyScalingFactor, isAsk, priceLimit, minBuyAmount);
} else if (action == uint32(ISettlerActions.LFJTM.selector)) {
(address recipient, IERC20 sellToken, uint256 bps, address pool, bool zeroForOne, uint256 minBuyAmount) =
abi.decode(data, (address, IERC20, uint256, address, bool, uint256));
Expand Down
148 changes: 148 additions & 0 deletions src/core/Hanji.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.33;

import {IERC20} from "@forge-std/interfaces/IERC20.sol";
import {SafeTransferLib} from "../vendor/SafeTransferLib.sol";
import {UnsafeMath} from "../utils/UnsafeMath.sol";
import {FastLogic} from "../utils/FastLogic.sol";
import {Ternary} from "../utils/Ternary.sol";
import {revertTooMuchSlippage} from "./SettlerErrors.sol";

import {SettlerAbstract} from "../SettlerAbstract.sol";

interface IHanjiPool {
function placeOrder(
bool isAsk,
uint128 quantity,
uint72 price,
uint128 max_commission,
bool market_only,
bool post_only,
bool transfer_executed_tokens,
uint256 expires
)
external
payable
returns (uint64 order_id, uint128 executed_shares, uint128 executed_value, uint128 aggressive_fee);

function placeMarketOrderWithTargetValue(
bool isAsk,
uint128 target_token_y_value,
uint72 price,
uint128 max_commission,
bool transfer_executed_tokens,
uint256 expires
) external payable returns (uint128 executed_shares, uint128 executed_value, uint128 aggressive_fee);

function getConfig()
external
view
returns (
uint256 _scaling_factor_token_x,
uint256 _scaling_factor_token_y,
address _token_x,
address _token_y,
bool _supports_native_eth,
bool _is_token_x_weth,
address _ask_trie,
address _bid_trie,
uint64 _admin_commission_rate,
uint64 _total_aggressive_commission_rate,
uint64 _total_passive_commission_rate,
uint64 _passive_order_payout_rate,
bool _should_invoke_on_trade
);
}

library FastHanjiPool {
function placeMarketOrder(
IHanjiPool pool,
uint256 sendNativeScaling,
bool isAsk,
uint128 quantity,
uint72 priceLimit
) internal returns (uint256 executed) {
assembly ("memory-safe") {
let ptr := mload(0x40)
mstore(ptr, xor(0xad73d32e, mul(0x58603c62, isAsk))) // selector
mstore(add(0x20, ptr), isAsk)
mstore(add(0x40, ptr), and(0xffffffffffffffffffffffffffffffff, quantity))
mstore(add(0x60, ptr), and(0xffffffffffffffffff, priceLimit))
mstore(add(0x80, ptr), 0xffffffffffffffffffffffffffffffff) // max_commission
mstore(add(0xa0, ptr), 0x01) // market_only/transfer_executed_tokens
mstore(add(0xc0, ptr), sub(isAsk, 0x01)) // post_only/expires
mstore(add(0xe0, ptr), 0x01) // transfer_executed_tokens/ignored
mstore(add(0x100, ptr), not(0x00)) // expires/ignored

if iszero(call(gas(), pool, mul(sendNativeScaling, quantity), add(0x1c, ptr), 0x104, 0x00, 0x80)) {
returndatacopy(ptr, 0x00, returndatasize())
revert(ptr, returndatasize())
}

executed := mload(shl(0x06, isAsk))
executed := sub(executed, mload(0x60))

mstore(0x40, ptr)
mstore(0x60, 0x00)
}
}

function getToken(IHanjiPool pool, bool tokenY) internal view returns (IERC20 result) {
assembly ("memory-safe") {
let ptr := mload(0x40)

mstore(0x00, 0xc3f909d4) // IHanjiPool.getConfig.selector
if iszero(staticcall(gas(), pool, 0x1c, 0x04, 0x00, 0x80)) {
returndatacopy(ptr, 0x00, returndatasize())
revert(ptr, returndatasize())
}

result := mload(add(0x40, shl(0x05, tokenY)))

mstore(0x40, ptr)
mstore(0x60, 0x00)
}
}
}

abstract contract Hanji is SettlerAbstract {
using FastHanjiPool for IHanjiPool;
using SafeTransferLib for IERC20;
using UnsafeMath for uint256;
using FastLogic for bool;
using Ternary for bool;

function sellToHanji(
IERC20 sellToken,
uint256 bps,
address pool,
uint256 sellScalingFactor,
uint256 buyScalingFactor,
bool isAsk,
uint256 priceLimit,
uint256 minBuyAmount
) internal returns (uint256 buyAmount) {
bool sendNative = sellToken == ETH_ADDRESS;
uint256 sellAmount;
unchecked {
if (sendNative) {
sellAmount = address(this).balance * bps / BASIS;
} else {
sellAmount = sellToken.fastBalanceOf(address(this)) * bps / BASIS;
sellToken.safeApproveIfBelow(pool, sellAmount);
}
}

uint256 scaledSellAmount = sellAmount.unsafeDiv(sellScalingFactor);

unchecked {
buyAmount = IHanjiPool(pool)
.placeMarketOrder(
sendNative.orZero(sellScalingFactor), isAsk, uint128(scaledSellAmount), uint72(priceLimit)
) * buyScalingFactor;
}
if (buyAmount < minBuyAmount) {
revertTooMuchSlippage(IHanjiPool(pool).getToken(isAsk), minBuyAmount, buyAmount);
}
}
}
4 changes: 3 additions & 1 deletion test/integration/BasePairTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,9 @@ abstract contract BasePairTest is Test, GasSnapshot, Permit2Signature, MainnetDe
}

function _balanceOf(IERC20 token, address account) external view {
uint256 result = token.balanceOf(account);
uint256 result = address(token) == 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE
? account.balance
: token.balanceOf(account);
assembly ("memory-safe") {
mstore(0x00, result)
revert(0x00, 0x20)
Expand Down
Loading