RainSwapper

0xd5adc179b5f9a1e2ab2e398b84881c71918c0529

Verification
Verified
v0.8.28+commit.7893614a
Type
Contract
4,378 bytes
ABI entries
23
4 read · 4 write
License
none

Contract information

Address
0xd5adc179b5f9a1e2ab2e398b84881c71918c0529
Chain
Robinhood Chain (4663)
Compiler
v0.8.28+commit.7893614a
Optimization
Enabled
Creation tx
0x52c687e7c5…b788b16796

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (4)

dexFactory()address
feeTier()uint24
poolOf(address)address
weth()address

Events (2)

SwapBuySwapSell

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "positionManager_",
        "type": "address"
      },
      {
        "internalType": "uint24",
        "name": "feeTier_",
        "type": "uint24"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "EthTransferFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "Expired",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NoPool",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotPool",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotSelf",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "SlippageExceeded",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "SnipeCapReached",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "SwapFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAmount",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "buyer",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethIn",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokensOut",
        "type": "uint256"
      }
    ],
    "name": "SwapBuy",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "seller",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokensIn",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethOut",
        "type": "uint256"
      }
    ],
    "name": "SwapSell",
    "type": "event"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "minTokensOut",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      }
    ],
    "name": "buy",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "tokensOut",
        "type": "uint256"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "dexFactory",
    "outputs": [
      {
        "internalType": "contract IUniswapV3Factory",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "feeTier",
    "outputs": [
      {
        "internalType": "uint24",
        "name": "",
        "type": "uint24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "poolOf",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokensIn",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minEthOut",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      }
    ],
    "name": "sell",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "ethOut",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "contract IUniswapV3Pool",
        "name": "pool",
        "type": "address"
      },
      {
        "internalType": "bool",
        "name": "zeroForOne",
        "type": "bool"
      },
      {
        "internalType": "uint256",
        "name": "amountIn",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      }
    ],
    "name": "swapExactIn",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "out",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "int256",
        "name": "amount0Delta",
        "type": "int256"
      },
      {
        "internalType": "int256",
        "name": "amount1Delta",
        "type": "int256"
      },
      {
        "internalType": "bytes",
        "name": "",
        "type": "bytes"
      }
    ],
    "name": "uniswapV3SwapCallback",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "weth",
    "outputs": [
      {
        "internalType": "contract IWETH9",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "stateMutability": "payable",
    "type": "receive"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {
    INonfungiblePositionManager,
    IUniswapV3Factory,
    IUniswapV3Pool,
    IWETH9
} from "./interfaces/IExternal.sol";
import {IRainLaunchTokenViews} from "./interfaces/IRainLaunch.sol";

contract RainSwapper is ReentrancyGuard {
    using SafeERC20 for IERC20;

    IUniswapV3Factory public immutable dexFactory;
    IWETH9 public immutable weth;
    uint24 public immutable feeTier;

    uint160 internal constant MIN_SQRT_RATIO = 4295128739;
    uint160 internal constant MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970342;

    address private _expectedPool;

    event SwapBuy(address indexed token, address indexed buyer, uint256 ethIn, uint256 tokensOut);
    event SwapSell(address indexed token, address indexed seller, uint256 tokensIn, uint256 ethOut);

    error NoPool();
    error ZeroAmount();
    error Expired();
    error SlippageExceeded();
    error NotPool();
    error NotSelf();
    error EthTransferFailed();
    error SnipeCapReached();
    error SwapFailed();

    constructor(address positionManager_, uint24 feeTier_) {
        INonfungiblePositionManager npm = INonfungiblePositionManager(positionManager_);
        dexFactory = IUniswapV3Factory(npm.factory());
        weth = IWETH9(npm.WETH9());
        feeTier = feeTier_;
    }

    function buy(address token, uint256 minTokensOut, uint256 deadline)
        external
        payable
        nonReentrant
        returns (uint256 tokensOut)
    {
        if (block.timestamp > deadline) revert Expired();
        if (msg.value == 0) revert ZeroAmount();
        IUniswapV3Pool pool = _poolOf(token);
        bool tokenIsToken0 = token < address(weth);

        weth.deposit{value: msg.value}();
        _expectedPool = address(pool);
        uint256 wethUsed = msg.value;
        try RainSwapper(payable(address(this))).swapExactIn(pool, !tokenIsToken0, msg.value, msg.sender)
        returns (uint256 out) {
            tokensOut = out;
        } catch {
            (uint256 allowed, bool capContext) = _snipeAllowance(token, msg.sender);
            if (!capContext) {
                _expectedPool = address(0);
                revert SwapFailed();
            }
            if (allowed == 0) {
                _expectedPool = address(0);
                revert SnipeCapReached();
            }
            (int256 a0, int256 a1) = pool.swap(
                msg.sender,
                !tokenIsToken0,
                -int256(allowed),
                tokenIsToken0 ? MAX_SQRT_RATIO - 1 : MIN_SQRT_RATIO + 1,
                ""
            );
            tokensOut = uint256(-(tokenIsToken0 ? a0 : a1));
            wethUsed = uint256(tokenIsToken0 ? a1 : a0);
        }
        _expectedPool = address(0);

        if (wethUsed == msg.value) {
            if (tokensOut < minTokensOut) revert SlippageExceeded();
        } else if (tokensOut * msg.value < minTokensOut * wethUsed) {
            revert SlippageExceeded();
        }

        uint256 wethLeft = weth.balanceOf(address(this));
        if (wethLeft > 0) {
            weth.withdraw(wethLeft);
            (bool ok,) = msg.sender.call{value: wethLeft}("");
            if (!ok) revert EthTransferFailed();
        }
        emit SwapBuy(token, msg.sender, msg.value - wethLeft, tokensOut);
    }

    function swapExactIn(IUniswapV3Pool pool, bool zeroForOne, uint256 amountIn, address recipient)
        external
        returns (uint256 out)
    {
        if (msg.sender != address(this)) revert NotSelf();
        (int256 a0, int256 a1) = pool.swap(
            recipient,
            zeroForOne,
            int256(amountIn),
            zeroForOne ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1,
            ""
        );
        out = uint256(-(zeroForOne ? a1 : a0));
    }

    function _snipeAllowance(address token, address to)
        internal
        view
        returns (uint256 allowed, bool capContext)
    {
        try IRainLaunchTokenViews(token).restrictionEndBlock() returns (uint256 endBlock) {
            if (block.number > endBlock) return (0, false);
            IRainLaunchTokenViews t = IRainLaunchTokenViews(token);
            address key = t.restrictionKeyOf(to);
            // Holdings-based room: how much more this wallet may hold before hitting maxWallet.
            // No cumulative counter — a wallet that sold back down gets its full room again.
            uint256 wallet = t.maxWalletLimit();
            uint256 bal = t.balanceOf(key);
            return (wallet > bal ? wallet - bal : 0, true);
        } catch {
            return (0, false);
        }
    }

    function sell(address token, uint256 tokensIn, uint256 minEthOut, uint256 deadline)
        external
        nonReentrant
        returns (uint256 ethOut)
    {
        if (block.timestamp > deadline) revert Expired();
        if (tokensIn == 0 || tokensIn > uint256(type(int256).max)) revert ZeroAmount();
        IUniswapV3Pool pool = _poolOf(token);
        bool tokenIsToken0 = token < address(weth);

        IERC20(token).safeTransferFrom(msg.sender, address(this), tokensIn);
        _expectedPool = address(pool);
        (int256 a0, int256 a1) = pool.swap(
            address(this),
            tokenIsToken0,
            int256(tokensIn),
            tokenIsToken0 ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1,
            ""
        );
        _expectedPool = address(0);

        ethOut = uint256(-(tokenIsToken0 ? a1 : a0));
        if (ethOut < minEthOut) revert SlippageExceeded();

        uint256 tokenLeft = IERC20(token).balanceOf(address(this));
        if (tokenLeft > 0) IERC20(token).safeTransfer(msg.sender, tokenLeft);

        weth.withdraw(ethOut);
        (bool ok,) = msg.sender.call{value: ethOut}("");
        if (!ok) revert EthTransferFailed();
        emit SwapSell(token, msg.sender, tokensIn - tokenLeft, ethOut);
    }

    function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata) external {
        address pool = _expectedPool;
        if (pool == address(0) || msg.sender != pool) revert NotPool();
        if (amount0Delta > 0) {
            IERC20(IUniswapV3Pool(pool).token0()).safeTransfer(pool, uint256(amount0Delta));
        }
        if (amount1Delta > 0) {
            IERC20(IUniswapV3Pool(pool).token1()).safeTransfer(pool, uint256(amount1Delta));
        }
    }

    function poolOf(address token) external view returns (address) {
        return dexFactory.getPool(token, address(weth), feeTier);
    }

    function _poolOf(address token) internal view returns (IUniswapV3Pool pool) {
        address p = dexFactory.getPool(token, address(weth), feeTier);
        if (p == address(0)) revert NoPool();
        pool = IUniswapV3Pool(p);
    }

    receive() external payable {
        if (msg.sender != address(weth)) revert EthTransferFailed();
    }
}
Chain explorer1742msChain node98ms