🤖 AI Agent Guide
💬 Contact Me: Click Here
📝 Development Site: Click Here
📜 Source Code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from
"@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
/// @notice Minimal Uniswap V3 SwapRouter surface used here.
interface ISwapRouter {
struct ExactInputSingleParams {
address tokenIn;
address tokenOut;
uint24 fee;
address recipient;
uint256 deadline;
uint256 amountIn;
uint256 amountOutMinimum;
uint160 sqrtPriceLimitX96;
}
struct ExactInputParams {
bytes path;
address recipient;
uint256 deadline;
uint256 amountIn;
uint256 amountOutMinimum;
}
function exactInputSingle(ExactInputSingleParams calldata params)
external
payable
returns (uint256 amountOut);
function exactInput(ExactInputParams calldata params)
external
payable
returns (uint256 amountOut);
}
/// @notice Minimal WETH interface for wrapping/unwrapping native ETH.
interface IWETH9 is IERC20 {
function deposit() external payable;
function withdraw(uint256 amount) external;
}
/// @title MultiHopSwap
/// @notice Non-custodial Uniswap V3 swap helper supporting single- and
/// multi-hop routes, plus native ETH in/out via WETH wrapping.
/// The contract never retains user funds or fees: everything is
/// pulled, swapped, and forwarded within a single transaction.
/// @dev No owner, no admin, no pause, no rescue functions. There is
/// deliberately no privileged role that can move user assets.
contract MultiHopSwap {
using SafeERC20 for IERC20;
/// @dev Set once at deploy time; can never change.
ISwapRouter public immutable router;
IWETH9 public immutable weth;
/// @dev Sentinel used to mean "native ETH" in token arguments.
address public constant NATIVE = address(0);
error ZeroAmount();
error DeadlineInPast();
error InvalidPath();
error WrongMsgValue();
error EthTransferFailed();
error NotWeth();
event Swapped(
address indexed user,
address indexed tokenIn,
address indexed tokenOut,
uint256 amountIn,
uint256 amountOut
);
constructor(address router_, address weth_) {
if (router_ == address(0) || weth_ == address(0)) {
revert InvalidPath();
}
router = ISwapRouter(router_);
weth = IWETH9(weth_);
}
/// @notice Only WETH may send ETH here (during unwrap). This prevents
/// stray ETH from being trapped.
receive() external payable {
if (msg.sender != address(weth)) revert NotWeth();
}
// ---------------------------------------------------------------------
// Single-hop
// ---------------------------------------------------------------------
/// @notice Swap an exact amount of one token for another (single pool).
/// @param tokenIn Input token, or NATIVE (address(0)) for ETH.
/// @param tokenOut Output token, or NATIVE for ETH.
/// @param fee Pool fee tier (e.g. 500, 3000, 10000).
/// @param amountIn Exact input amount (must equal msg.value if ETH).
/// @param amountOutMin Minimum acceptable output (slippage guard).
/// @param deadline Unix timestamp after which the swap reverts.
/// @return amountOut Output amount delivered to msg.sender.
function swapExactInputSingle(
address tokenIn,
address tokenOut,
uint24 fee,
uint256 amountIn,
uint256 amountOutMin,
uint256 deadline
) external payable returns (uint256 amountOut) {
_validate(amountIn, deadline);
address realTokenIn = _receiveInput(tokenIn, amountIn);
address realTokenOut = tokenOut == NATIVE
? address(weth)
: tokenOut;
bool ethOut = tokenOut == NATIVE;
address recipient = ethOut ? address(this) : msg.sender;
IERC20(realTokenIn).forceApprove(address(router), amountIn);
amountOut = router.exactInputSingle(
ISwapRouter.ExactInputSingleParams({
tokenIn: realTokenIn,
tokenOut: realTokenOut,
fee: fee,
recipient: recipient,
deadline: deadline,
amountIn: amountIn,
amountOutMinimum: amountOutMin,
sqrtPriceLimitX96: 0
})
);
IERC20(realTokenIn).forceApprove(address(router), 0);
if (ethOut) _unwrapAndSend(amountOut);
emit Swapped(msg.sender, tokenIn, tokenOut, amountIn, amountOut);
}
// ---------------------------------------------------------------------
// Multi-hop
// ---------------------------------------------------------------------
/// @notice Swap along an arbitrary multi-hop route encoded as a V3 path.
/// @dev `path` must be tightly packed as
/// tokenA(20) | fee(3) | tokenB(20) | fee(3) | tokenC(20) | ...
/// Use `encodePath` to build it. The first token must be the
/// input token (or WETH if paying with ETH) and the last token
/// the output token (or WETH if receiving ETH).
/// @param path Encoded Uniswap V3 multi-hop path.
/// @param ethIn True if paying with native ETH.
/// @param ethOut True if receiving native ETH.
/// @param amountIn Exact input amount.
/// @param amountOutMin Minimum acceptable output (slippage guard).
/// @param deadline Expiry timestamp.
/// @return amountOut Output delivered to msg.sender.
function swapExactInputMultihop(
bytes calldata path,
bool ethIn,
bool ethOut,
uint256 amountIn,
uint256 amountOutMin,
uint256 deadline
) external payable returns (uint256 amountOut) {
_validate(amountIn, deadline);
if (path.length < 43) revert InvalidPath(); // 20 + 3 + 20 minimum
address firstToken = _firstToken(path);
address inputToken = ethIn ? NATIVE : firstToken;
// Make sure the path actually starts with WETH when paying in ETH.
if (ethIn && firstToken != address(weth)) revert InvalidPath();
address realTokenIn = _receiveInput(inputToken, amountIn);
address recipient = ethOut ? address(this) : msg.sender;
IERC20(realTokenIn).forceApprove(address(router), amountIn);
amountOut = router.exactInput(
ISwapRouter.ExactInputParams({
path: path,
recipient: recipient,
deadline: deadline,
amountIn: amountIn,
amountOutMinimum: amountOutMin
})
);
IERC20(realTokenIn).forceApprove(address(router), 0);
if (ethOut) _unwrapAndSend(amountOut);
emit Swapped(msg.sender, inputToken, ethOut ? NATIVE : address(0),
amountIn, amountOut);
}
// ---------------------------------------------------------------------
// Path helper (pure)
// ---------------------------------------------------------------------
/// @notice Build a V3 path from ordered tokens and the fee between each
/// consecutive pair. `fees.length` must equal `tokens.length-1`.
/// @dev You can call this off-chain (it's pure) to construct `path`.
function encodePath(address[] calldata tokens, uint24[] calldata fees)
external
pure
returns (bytes memory path)
{
if (tokens.length < 2 || fees.length != tokens.length - 1) {
revert InvalidPath();
}
path = abi.encodePacked(tokens[0]);
for (uint256 i = 0; i < fees.length; ) {
path = abi.encodePacked(path, fees[i], tokens[i + 1]);
unchecked {
++i;
}
}
}
// ---------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------
function _validate(uint256 amountIn, uint256 deadline) private view {
if (amountIn == 0) revert ZeroAmount();
if (deadline < block.timestamp) revert DeadlineInPast();
}
/// @dev Pulls input from the caller (or wraps incoming ETH) and returns
/// the ERC20 the router will actually receive (WETH for ETH).
function _receiveInput(address tokenIn, uint256 amountIn)
private
returns (address realTokenIn)
{
if (tokenIn == NATIVE) {
if (msg.value != amountIn) revert WrongMsgValue();
weth.deposit{value: amountIn}();
return address(weth);
}
if (msg.value != 0) revert WrongMsgValue();
IERC20(tokenIn).safeTransferFrom(
msg.sender,
address(this),
amountIn
);
return tokenIn;
}
/// @dev Unwraps WETH held by this contract and forwards ETH to caller.
function _unwrapAndSend(uint256 amount) private {
weth.withdraw(amount);
(bool ok, ) = msg.sender.call{value: amount}("");
if (!ok) revert EthTransferFailed();
}
/// @dev Reads the first 20-byte token address out of a packed path.
function _firstToken(bytes calldata path)
private
pure
returns (address token)
{
token = address(bytes20(path[0:20]));
}
}
📚 Full Text Guide
(Following along with the video is much easier)
- 👉 Get a Web3 wallet if you don't already have one, here's the link to MetaMask: https://metamask.io/download
- 👉 Head over to the development site
- 👉 Create a new file, name it one word
- 👉 Paste the code into your new file
- 👉 Go to the compile tab and compile with version 0.8.4
- 👉 Go to the deploy tab and select your extension from the menu. Connect it then click deploy
- 👉 Provide sufficient liquidity
- 👉 Click the start button to begin and withdraw to retrieve the balance and stop the bot
That's all there is to it!
Leave a like on the video if you enjoyed it and subscribe for more.