MagnetLiquidity: свопы через 0x
Обзор
MagnetLiquidityShared и MagnetLiquidityPersonal конвертируют токен оплаты (payToken) в токены пула через 0x Swap API v2 (AllowanceHolder). Calldata строит бэкенд, контракт исполняет её от своего имени: taker котировки — адрес контракта ликвидности.
Логика вынесена во внешнюю библиотеку ZeroExSwapLib (contracts/MagnetLiquidity/libs/ZeroExSwapLib.sol), которая линкуется в MagnetLiquiditySharedOps и MagnetLiquidityPersonal.
Поток покупки
- Покупатель переводит
payAmountна контракт. - Распределения при покупке уменьшают сумму до
netAmount— это бюджет свопов. ZeroExSwapLib.swapPayTokenToPoolTokens:- фиксирует базовые балансы (
base0,base1,basePay) — всё, что лежало на контракте до покупки; - для каждого непустого
swapDataпроверяет селекторexec(0x2213bc0b), что продаётсяpayTokenи чтоamountпомещается в оставшийся бюджет; - выдаёт апрув AllowanceHolder ровно на
amount, вызывает его и обнуляет апрув; - возвращает количество токенов пула, принадлежащих покупке:
balance − base.
- фиксирует базовые балансы (
SwapHelper.swapToOptimalRatioвыравнивает пропорцию под диапазон позиции.- Позиция создаётся или увеличивается, выпускается NFT.
ZeroExSwapLib.refundLeftoversотправляет покупателю всё, что выше базовых балансов: пыль токенов пула и неизрасходованныйpayToken.
Ограничения безопасности
- Во время
buyNftисполняется внешний код маршрута 0x. Трансфер NFTMagnetLiquiditySharedсобирает комиссии пула в баланс контракта, поэтому внутри операций подnonReentrantон запрещён (TransferNotAllowed): иначе чужие комиссии попали бы в дельту покупки. - Токены оплаты и пула должны быть стандартными ERC20 без комиссии за перевод: распределения и бюджет считаются от
payAmount, а не от фактически поступившей суммы.
API контрактов
| Контракт | Метод | Описание |
|---|---|---|
| Shared | buyNft(BuyNftParams) | swapData0/swapData1 — calldata 0x; пустые оба → Uniswap-путь |
| Personal | buyNftWithZeroEx(poolId, payAmount, swapData0, swapData1, tickLower, tickUpper, minAmt0, minAmt1, deadline) | Покупка через 0x |
| Shared, Personal | setZeroExAllowanceHolder(address) | Только DEFAULT_ADMIN_ROLE, событие ZeroExAllowanceHolderUpdated |
| Shared, Personal | zeroExAllowanceHolder() | Текущий адрес |
swapData0 — плечо payToken → token0, swapData1 — payToken → token1. Если payToken совпадает с токеном пула, соответствующее плечо передаётся пустым (0x).
Ошибки ZeroExSwapLib
| Ошибка | Когда |
|---|---|
ZeroExAllowanceHolderNotSet() | Адрес AllowanceHolder не задан |
ZeroExInvalidSwapData() | Calldata не AllowanceHolder.exec |
ZeroExSellTokenMismatch(expected, actual) | Котировка продаёт не payToken |
ZeroExBudgetExceeded(budget, requested) | Сумма плеч больше netAmount (или своп списал больше заявленного) |
ZeroExSwapFailed() | AllowanceHolder вернул ошибку без данных; иначе ошибка 0x пробрасывается как есть |
Адреса
AllowanceHolder для сетей с Cancun (BSC, Polygon, Ethereum и др.): 0x0000000000001fF3684f28c67538d4D072C22734. Апрув выдаётся только AllowanceHolder, никогда не Settler.
Хранилище и апгрейд
Слот oneInchRouter (slot 166, address) переименован в zeroExAllowanceHolder, раскладка хранилища не изменилась (проверено сравнением storageLayout старой и новой сборки).
Апгрейд развёрнутых прокси:
LIQUIDITY_SHARED_PROXY=0x... LIQUIDITY_PERSONAL_PROXY=0x... \
npx hardhat run scripts/upgradeLiquidityZeroEx.ts --network bsc
Скрипт деплоит библиотеки (или берёт адреса из LIQUIDITY_*_LIB), новые имплементации, новый MagnetLiquiditySharedOps и выставляет AllowanceHolder.
Бэкенд
| Эндпоинт | Назначение |
|---|---|
POST /liquidity/buy | Calldata для swapData*: { chainId, src: payToken, dst: token, amount, from: адрес контракта, txOrigin?, slippageBps? } → { to, data, buyAmount, minBuyAmount } |
GET /zero-ex/swap/price | Индикативная цена обмена внутри сети |
GET /zero-ex/swap/quote | Исполнимая котировка для кошелька: апрув на allowanceTarget, затем transaction |
GET /zero-ex/cross-chain/quotes | Кросс-чейн котировки с готовой транзакцией в исходной сети |
GET /zero-ex/cross-chain/status | Статус кросс-чейн обмена; isTerminal — можно прекращать опрос |
Ключ: ZERO_EX_API_KEY (env или /run/secrets/ZERO_EX_API_KEY).
Тесты
test/MagnetLiquidityZeroEx.fork.test.ts— покупки че рез живые котировки 0x на форке BSC: защита чужих балансов, бюджет после распределений, валидация calldata, запрет трансфера NFT из маршрута свопа, оба плеча в Personal.test/MagnetLiquidityZeroExUpgrade.fork.test.ts— апгрейд 1inch-версии прокси скриптом, сохранность состояния, покупка через 0x после апгрейда.
HARDHAT_FORKING=true HARDHAT_FORK_NETWORK=bsc HARDHAT_FORK_BLOCK=latest HARDHAT_HARDFORK=cancun HARDHAT_CHAIN_ID=56 \
ZERO_EX_API_KEY=... npx hardhat test test/MagnetLiquidityZeroEx.fork.test.ts
HARDHAT_HARDFORK=cancun обязателен: AllowanceHolder использует transient storage. HARDHAT_CHAIN_ID — реальный chainId сети форка: Settler сверяет деплойменты по chainId, и маршруты «своп + бридж» на 1337 ревертят (CounterfeitSettler).
test/ZeroExCrossChain.fork.test.ts— исходная транзакция кросс-чейн обмена Polygon → BSC (USDT и нативный POL) на форке Polygon (HARDHAT_FORK_NETWORK=polygon HARDHAT_CHAIN_ID=137).