Skip to content

Commit a5b36eb

Browse files
committed
Merge branch 'dcmt/hanji'
2 parents 1c7d8bf + b11987c commit a5b36eb

File tree

10 files changed

+520
-3
lines changed

10 files changed

+520
-3
lines changed

.forge-snapshots/hanji_sellNativeForUsdc_hanji_wmon_to_usdc.snap

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.forge-snapshots/hanji_sellUsdcForWmon_hanji_usdc_to_wmon.snap

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.forge-snapshots/hanji_sellWmonForUsdc_hanji_wmon_to_usdc.snap

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ Master list of UniV3 forks:
5050

5151
### Non-breaking changes
5252

53+
* Add `HANJI` action for Hanji order book liquidity source on Base and Monad
54+
* Note that while it is possible to sell eith ETH (native) or WETH (wrapped
55+
native) to Hanji pools with the wrapped native asset as one of the tokens,
56+
attempting to buy WETH (wrapped native) is not possible. You will always get
57+
raw ETH (native).
58+
5359
## 2025-12-29
5460

5561
### Breaking changes

src/ISettlerActions.sol

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,4 +322,15 @@ interface ISettlerActions {
322322
BebopMakerSignature memory makerSignature,
323323
uint256 amountOutMin
324324
) external;
325+
326+
function HANJI(
327+
address sellToken,
328+
uint256 bps,
329+
address pool,
330+
uint256 sellScalingFactor,
331+
uint256 buyScalingFactor,
332+
bool isAsk,
333+
uint256 priceLimit,
334+
uint256 minBuyAmount
335+
) external;
325336
}

src/chains/Base/Common.sol

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {BalancerV3} from "../../core/BalancerV3.sol";
1313
import {PancakeInfinity} from "../../core/PancakeInfinity.sol";
1414
import {Renegade, BASE_SELECTOR} from "../../core/Renegade.sol";
1515
import {Bebop} from "../../core/Bebop.sol";
16+
import {Hanji} from "../../core/Hanji.sol";
1617

1718
import {IMsgSender} from "../../interfaces/IMsgSender.sol";
1819
import {FreeMemory} from "../../utils/FreeMemory.sol";
@@ -70,7 +71,8 @@ abstract contract BaseMixin is
7071
PancakeInfinity,
7172
//EulerSwap,
7273
Renegade,
73-
Bebop
74+
Bebop,
75+
Hanji
7476
{
7577
using FastLogic for bool;
7678

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

168170
sellToRenegade(target, baseToken, renegadeData);
171+
} else if (action == uint32(ISettlerActions.HANJI.selector)) {
172+
(
173+
IERC20 sellToken,
174+
uint256 bps,
175+
address pool,
176+
uint256 sellScalingFactor,
177+
uint256 buyScalingFactor,
178+
bool isAsk,
179+
uint256 priceLimit,
180+
uint256 minBuyAmount
181+
) = abi.decode(data, (IERC20, uint256, address, uint256, uint256, bool, uint256, uint256));
182+
183+
sellToHanji(sellToken, bps, pool, sellScalingFactor, buyScalingFactor, isAsk, priceLimit, minBuyAmount);
169184
} else {
170185
return false;
171186
}

src/chains/Monad/Common.sol

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {SettlerBase} from "../../SettlerBase.sol";
66
import {IERC20} from "@forge-std/interfaces/IERC20.sol";
77
import {UniswapV4} from "../../core/UniswapV4.sol";
88
import {BalancerV3} from "../../core/BalancerV3.sol";
9+
import {Hanji} from "../../core/Hanji.sol";
910
import {LfjTokenMill} from "../../core/LfjTokenMill.sol";
1011
import {FreeMemory} from "../../utils/FreeMemory.sol";
1112

@@ -33,7 +34,7 @@ import {MONAD_POOL_MANAGER} from "../../core/UniswapV4Addresses.sol";
3334
import {SettlerAbstract} from "../../SettlerAbstract.sol";
3435
import {Permit2PaymentAbstract} from "../../core/Permit2PaymentAbstract.sol";
3536

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

7576
sellToBalancerV3(recipient, sellToken, bps, feeOnTransfer, hashMul, hashMod, fills, amountOutMin);
77+
} else if (action == uint32(ISettlerActions.HANJI.selector)) {
78+
(
79+
IERC20 sellToken,
80+
uint256 bps,
81+
address pool,
82+
uint256 sellScalingFactor,
83+
uint256 buyScalingFactor,
84+
bool isAsk,
85+
uint256 priceLimit,
86+
uint256 minBuyAmount
87+
) = abi.decode(data, (IERC20, uint256, address, uint256, uint256, bool, uint256, uint256));
88+
89+
sellToHanji(sellToken, bps, pool, sellScalingFactor, buyScalingFactor, isAsk, priceLimit, minBuyAmount);
7690
} else if (action == uint32(ISettlerActions.LFJTM.selector)) {
7791
(address recipient, IERC20 sellToken, uint256 bps, address pool, bool zeroForOne, uint256 minBuyAmount) =
7892
abi.decode(data, (address, IERC20, uint256, address, bool, uint256));

src/core/Hanji.sol

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.33;
3+
4+
import {IERC20} from "@forge-std/interfaces/IERC20.sol";
5+
import {SafeTransferLib} from "../vendor/SafeTransferLib.sol";
6+
import {UnsafeMath} from "../utils/UnsafeMath.sol";
7+
import {FastLogic} from "../utils/FastLogic.sol";
8+
import {Ternary} from "../utils/Ternary.sol";
9+
import {revertTooMuchSlippage} from "./SettlerErrors.sol";
10+
11+
import {SettlerAbstract} from "../SettlerAbstract.sol";
12+
13+
interface IHanjiPool {
14+
function placeOrder(
15+
bool isAsk,
16+
uint128 quantity,
17+
uint72 price,
18+
uint128 max_commission,
19+
bool market_only,
20+
bool post_only,
21+
bool transfer_executed_tokens,
22+
uint256 expires
23+
)
24+
external
25+
payable
26+
returns (uint64 order_id, uint128 executed_shares, uint128 executed_value, uint128 aggressive_fee);
27+
28+
function placeMarketOrderWithTargetValue(
29+
bool isAsk,
30+
uint128 target_token_y_value,
31+
uint72 price,
32+
uint128 max_commission,
33+
bool transfer_executed_tokens,
34+
uint256 expires
35+
) external payable returns (uint128 executed_shares, uint128 executed_value, uint128 aggressive_fee);
36+
37+
function getConfig()
38+
external
39+
view
40+
returns (
41+
uint256 _scaling_factor_token_x,
42+
uint256 _scaling_factor_token_y,
43+
address _token_x,
44+
address _token_y,
45+
bool _supports_native_eth,
46+
bool _is_token_x_weth,
47+
address _ask_trie,
48+
address _bid_trie,
49+
uint64 _admin_commission_rate,
50+
uint64 _total_aggressive_commission_rate,
51+
uint64 _total_passive_commission_rate,
52+
uint64 _passive_order_payout_rate,
53+
bool _should_invoke_on_trade
54+
);
55+
}
56+
57+
library FastHanjiPool {
58+
function placeMarketOrder(
59+
IHanjiPool pool,
60+
uint256 sendNativeScaling,
61+
bool isAsk,
62+
uint128 quantity,
63+
uint72 priceLimit
64+
) internal returns (uint256 executed) {
65+
assembly ("memory-safe") {
66+
let ptr := mload(0x40)
67+
mstore(ptr, xor(0xad73d32e, mul(0x58603c62, isAsk))) // selector
68+
mstore(add(0x20, ptr), isAsk)
69+
mstore(add(0x40, ptr), and(0xffffffffffffffffffffffffffffffff, quantity))
70+
mstore(add(0x60, ptr), and(0xffffffffffffffffff, priceLimit))
71+
mstore(add(0x80, ptr), 0xffffffffffffffffffffffffffffffff) // max_commission
72+
mstore(add(0xa0, ptr), 0x01) // market_only/transfer_executed_tokens
73+
mstore(add(0xc0, ptr), sub(isAsk, 0x01)) // post_only/expires
74+
mstore(add(0xe0, ptr), 0x01) // transfer_executed_tokens/ignored
75+
mstore(add(0x100, ptr), not(0x00)) // expires/ignored
76+
77+
if iszero(call(gas(), pool, mul(sendNativeScaling, quantity), add(0x1c, ptr), 0x104, 0x00, 0x80)) {
78+
returndatacopy(ptr, 0x00, returndatasize())
79+
revert(ptr, returndatasize())
80+
}
81+
82+
executed := mload(shl(0x06, isAsk))
83+
executed := sub(executed, mload(0x60))
84+
85+
mstore(0x40, ptr)
86+
mstore(0x60, 0x00)
87+
}
88+
}
89+
90+
function getToken(IHanjiPool pool, bool tokenY) internal view returns (IERC20 result) {
91+
assembly ("memory-safe") {
92+
let ptr := mload(0x40)
93+
94+
mstore(0x00, 0xc3f909d4) // IHanjiPool.getConfig.selector
95+
if iszero(staticcall(gas(), pool, 0x1c, 0x04, 0x00, 0x80)) {
96+
returndatacopy(ptr, 0x00, returndatasize())
97+
revert(ptr, returndatasize())
98+
}
99+
100+
result := mload(add(0x40, shl(0x05, tokenY)))
101+
102+
mstore(0x40, ptr)
103+
mstore(0x60, 0x00)
104+
}
105+
}
106+
}
107+
108+
abstract contract Hanji is SettlerAbstract {
109+
using FastHanjiPool for IHanjiPool;
110+
using SafeTransferLib for IERC20;
111+
using UnsafeMath for uint256;
112+
using FastLogic for bool;
113+
using Ternary for bool;
114+
115+
function sellToHanji(
116+
IERC20 sellToken,
117+
uint256 bps,
118+
address pool,
119+
uint256 sellScalingFactor,
120+
uint256 buyScalingFactor,
121+
bool isAsk,
122+
uint256 priceLimit,
123+
uint256 minBuyAmount
124+
) internal returns (uint256 buyAmount) {
125+
bool sendNative = sellToken == ETH_ADDRESS;
126+
uint256 sellAmount;
127+
unchecked {
128+
if (sendNative) {
129+
sellAmount = address(this).balance * bps / BASIS;
130+
} else {
131+
sellAmount = sellToken.fastBalanceOf(address(this)) * bps / BASIS;
132+
sellToken.safeApproveIfBelow(pool, sellAmount);
133+
}
134+
}
135+
136+
uint256 scaledSellAmount = sellAmount.unsafeDiv(sellScalingFactor);
137+
138+
unchecked {
139+
buyAmount = IHanjiPool(pool)
140+
.placeMarketOrder(
141+
sendNative.orZero(sellScalingFactor), isAsk, uint128(scaledSellAmount), uint72(priceLimit)
142+
) * buyScalingFactor;
143+
}
144+
if (buyAmount < minBuyAmount) {
145+
revertTooMuchSlippage(IHanjiPool(pool).getToken(isAsk), minBuyAmount, buyAmount);
146+
}
147+
}
148+
}

test/integration/BasePairTest.t.sol

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,9 @@ abstract contract BasePairTest is Test, GasSnapshot, Permit2Signature, MainnetDe
140140
}
141141

142142
function _balanceOf(IERC20 token, address account) external view {
143-
uint256 result = token.balanceOf(account);
143+
uint256 result = address(token) == 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE
144+
? account.balance
145+
: token.balanceOf(account);
144146
assembly ("memory-safe") {
145147
mstore(0x00, result)
146148
revert(0x00, 0x20)

0 commit comments

Comments
 (0)