Перейти к основному содержимому

MagnetLiquidity: свопы через 0x

Обзор​

MagnetLiquidityShared и MagnetLiquidityPersonal конвертируют токен оплаты (payToken) в токены пула через 0x Swap API v2 (AllowanceHolder). Calldata строит бэкенд, контракт исполняет её от своего имени: taker котировки — адрес контракта ликвидности.

Логика вынесена во внешнюю библиотеку ZeroExSwapLib (contracts/MagnetLiquidity/libs/ZeroExSwapLib.sol), которая линкуется в MagnetLiquiditySharedOps и MagnetLiquidityPersonal.

Поток покупки​

  1. Покупатель переводит payAmount на контракт.
  2. Распределения при покупке уменьшают сумму до netAmount — это бюджет свопов.
  3. ZeroExSwapLib.swapPayTokenToPoolTokens:
    • фиксирует базовые балансы (base0, base1, basePay) — всё, что лежало на контракте до покупки;
    • для каждого непустого swapData проверяет селектор exec (0x2213bc0b), что продаётся payToken и что amount помещается в оставшийся бюджет;
    • выдаёт апрув AllowanceHolder ровно на amount, вызывает его и обнуляет апрув;
    • возвращает количество токенов пула, принадлежащих покупке: balance − base.
  4. SwapHelper.swapToOptimalRatio выравнивает пропорцию под диапазон позиции.
  5. Позиция создаётся или увеличивается, выпускается NFT.
  6. ZeroExSwapLib.refundLeftovers отправляет покупателю всё, что выше базовых балансов: пыль токенов пула и неизрасходованный payToken.

Ограничения безопасности​

  • Во время buyNft исполняется внешний код маршрута 0x. Трансфер NFT MagnetLiquidityShared собирает комиссии пула в баланс контракта, поэтому внутри операций под nonReentrant он запрещён (TransferNotAllowed): иначе чужие комиссии попали бы в дельту покупки.
  • Токены оплаты и пула должны быть стандартными ERC20 без комиссии за перевод: распределения и бюджет считаются от payAmount, а не от фактически поступившей суммы.

API контрактов​

КонтрактМетодОписание
SharedbuyNft(BuyNftParams)swapData0/swapData1 — calldata 0x; пустые оба → Uniswap-путь
PersonalbuyNftWithZeroEx(poolId, payAmount, swapData0, swapData1, tickLower, tickUpper, minAmt0, minAmt1, deadline)Покупка через 0x
Shared, PersonalsetZeroExAllowanceHolder(address)Только DEFAULT_ADMIN_ROLE, событие ZeroExAllowanceHolderUpdated
Shared, PersonalzeroExAllowanceHolder()Текущий адрес

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/buyCalldata для 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).