SatoSwapRouter

0x821841dbd64cced4aab7004eaf0cda71e5bc7741

Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
3,248 bytes
ABI entries
12
1 read · 3 write
License
none

Contract information

Address
0x821841dbd64cced4aab7004eaf0cda71e5bc7741
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0x09a1af76d9…fa77dc0e16

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (1)

POOL_MANAGER()address

ABI

[
  {
    "inputs": [
      {
        "internalType": "contract IPoolManager",
        "name": "poolManager",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "BadMsgValue",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidDelta",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotPoolManager",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "Slippage",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TransferFromFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAmount",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "POOL_MANAGER",
    "outputs": [
      {
        "internalType": "contract IPoolManager",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "components": [
          {
            "internalType": "Currency",
            "name": "currency0",
            "type": "address"
          },
          {
            "internalType": "Currency",
            "name": "currency1",
            "type": "address"
          },
          {
            "internalType": "uint24",
            "name": "fee",
            "type": "uint24"
          },
          {
            "internalType": "int24",
            "name": "tickSpacing",
            "type": "int24"
          },
          {
            "internalType": "contract IHooks",
            "name": "hooks",
            "type": "address"
          }
        ],
        "internalType": "struct PoolKey",
        "name": "key",
        "type": "tuple"
      },
      {
        "internalType": "uint256",
        "name": "minRatoOut",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      }
    ],
    "name": "buy",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "ratoOut",
        "type": "uint256"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "components": [
          {
            "internalType": "Currency",
            "name": "currency0",
            "type": "address"
          },
          {
            "internalType": "Currency",
            "name": "currency1",
            "type": "address"
          },
          {
            "internalType": "uint24",
            "name": "fee",
            "type": "uint24"
          },
          {
            "internalType": "int24",
            "name": "tickSpacing",
            "type": "int24"
          },
          {
            "internalType": "contract IHooks",
            "name": "hooks",
            "type": "address"
          }
        ],
        "internalType": "struct PoolKey",
        "name": "key",
        "type": "tuple"
      },
      {
        "internalType": "uint256",
        "name": "ratoIn",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minEthOut",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      }
    ],
    "name": "sell",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "ethOut",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes",
        "name": "rawData",
        "type": "bytes"
      }
    ],
    "name": "unlockCallback",
    "outputs": [
      {
        "internalType": "bytes",
        "name": "",
        "type": "bytes"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "stateMutability": "payable",
    "type": "receive"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

import {IERC20Minimal} from "v4-core/interfaces/external/IERC20Minimal.sol";
import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol";
import {PoolKey} from "v4-core/types/PoolKey.sol";
import {SwapParams} from "v4-core/types/PoolOperation.sol";
import {BalanceDelta, BalanceDeltaLibrary} from "v4-core/types/BalanceDelta.sol";
import {Currency} from "v4-core/types/Currency.sol";

/// @notice Minimal router for the SATO hook. It pre-settles the exact input into the v4 PoolManager,
/// calls swap through the hook-backed pool, and takes the output back to the recipient.
contract SatoSwapRouter {
    using BalanceDeltaLibrary for BalanceDelta;

    uint160 internal constant MIN_SQRT_PRICE_PLUS_ONE = 4295128740;
    uint160 internal constant MAX_SQRT_PRICE_MINUS_ONE = 1461446703485210103287273052203988822378723970341;

    IPoolManager public immutable POOL_MANAGER;

    enum Action {
        Buy,
        Sell
    }

    struct CallbackData {
        Action action;
        PoolKey key;
        address payer;
        address recipient;
        uint256 amountIn;
        uint256 minAmountOut;
    }

    error NotPoolManager();
    error BadMsgValue();
    error ZeroAmount();
    error Slippage();
    error InvalidDelta();
    error TransferFromFailed();

    constructor(IPoolManager poolManager) {
        POOL_MANAGER = poolManager;
    }

    function buy(PoolKey calldata key, uint256 minRatoOut, address recipient)
        external
        payable
        returns (uint256 ratoOut)
    {
        if (msg.value == 0) revert ZeroAmount();
        if (recipient == address(0)) recipient = msg.sender;

        CallbackData memory data = CallbackData({
            action: Action.Buy,
            key: key,
            payer: msg.sender,
            recipient: recipient,
            amountIn: msg.value,
            minAmountOut: minRatoOut
        });

        ratoOut = abi.decode(POOL_MANAGER.unlock(abi.encode(data)), (uint256));
    }

    function sell(PoolKey calldata key, uint256 ratoIn, uint256 minEthOut, address recipient)
        external
        returns (uint256 ethOut)
    {
        if (ratoIn == 0) revert ZeroAmount();
        if (recipient == address(0)) recipient = msg.sender;

        CallbackData memory data = CallbackData({
            action: Action.Sell,
            key: key,
            payer: msg.sender,
            recipient: recipient,
            amountIn: ratoIn,
            minAmountOut: minEthOut
        });

        ethOut = abi.decode(POOL_MANAGER.unlock(abi.encode(data)), (uint256));
    }

    function unlockCallback(bytes calldata rawData) external returns (bytes memory) {
        if (msg.sender != address(POOL_MANAGER)) revert NotPoolManager();

        CallbackData memory data = abi.decode(rawData, (CallbackData));
        uint256 amountOut;

        if (data.action == Action.Buy) {
            amountOut = _buy(data);
        } else {
            amountOut = _sell(data);
        }

        if (amountOut < data.minAmountOut) revert Slippage();
        return abi.encode(amountOut);
    }

    function _buy(CallbackData memory data) internal returns (uint256 ratoOut) {
        if (address(this).balance < data.amountIn) revert BadMsgValue();
        POOL_MANAGER.settle{value: data.amountIn}();

        BalanceDelta delta = POOL_MANAGER.swap(
            data.key,
            SwapParams({
                zeroForOne: true,
                amountSpecified: -int256(data.amountIn),
                sqrtPriceLimitX96: MIN_SQRT_PRICE_PLUS_ONE
            }),
            abi.encode(data.payer)
        );

        int128 amount1 = delta.amount1();
        if (amount1 <= 0) revert InvalidDelta();
        ratoOut = uint128(amount1);
        POOL_MANAGER.take(data.key.currency1, data.recipient, ratoOut);
    }

    function _sell(CallbackData memory data) internal returns (uint256 ethOut) {
        Currency rato = data.key.currency1;
        POOL_MANAGER.sync(rato);
        bool success = IERC20Minimal(Currency.unwrap(rato)).transferFrom(data.payer, address(POOL_MANAGER), data.amountIn);
        if (!success) revert TransferFromFailed();
        POOL_MANAGER.settle();

        BalanceDelta delta = POOL_MANAGER.swap(
            data.key,
            SwapParams({
                zeroForOne: false,
                amountSpecified: -int256(data.amountIn),
                sqrtPriceLimitX96: MAX_SQRT_PRICE_MINUS_ONE
            }),
            abi.encode(data.payer)
        );

        int128 amount0 = delta.amount0();
        if (amount0 <= 0) revert InvalidDelta();
        ethOut = uint128(amount0);
        POOL_MANAGER.take(data.key.currency0, data.recipient, ethOut);
    }

    receive() external payable {}
}
Chain explorer2142msChain node71ms