TriangleFlashV3

Description:

Decentralized Finance (DeFi) protocol contract providing Swap, Factory functionality.

Blockchain: Ethereum

Source Code: View Code On The Blockchain

Solidity Source Code:

{{
  "language": "Solidity",
  "sources": {
    "TriangleFlashV3_V2.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/// @notice Minimal ERC20 interface
interface IERC20 {
    function balanceOf(address) external view returns (uint256);

    function approve(address spender, uint256 value) external returns (bool);

    function transfer(address dst, uint256 value) external returns (bool);

    function transferFrom(address src, address dst, uint256 value) external returns (bool);

    function allowance(address owner, address spender) external view returns (uint256);

    function decimals() external view returns (uint8);

    function symbol() external view returns (string memory);
}

/// @notice Uniswap V3 Router (Periphery) interface: exactInput(path,…)
interface ISwapRouterV3 {
    struct ExactInputParams {
        bytes path;            // tokenIn | fee | tokenMid | fee | tokenOut ...
        address recipient;     // receiver of final output
        uint256 deadline;      // timestamp
        uint256 amountIn;      // amount of tokenIn to swap
        uint256 amountOutMinimum; // slippage guard
    }

    function exactInput(ExactInputParams calldata params) external payable returns (uint256 amountOut);
}

/// @notice Minimal Uniswap V3 pool flash interface
interface IUniswapV3Pool {
    function token0() external view returns (address);

    function token1() external view returns (address);

    /// @dev Borrow tokens; must repay + fees in callback
    function flash(address recipient, uint256 amount0, uint256 amount1, bytes calldata data) external;
}

/// @notice Uniswap V3 flash callback interface
interface IUniswapV3FlashCallback {
    /// @dev Called by the pool after sending tokens; must repay amount + fee
    function uniswapV3FlashCallback(uint256 fee0, uint256 fee1, bytes calldata data) external;
}

/// @notice Ownable (lightweight)
abstract contract Ownable {
    event OwnershipTransferred(address indexed prev, address indexed next);

    address public owner;
    modifier onlyOwner() {
        require(msg.sender == owner, "not owner");
        _;
    }
    constructor() {owner = msg.sender;
        emit OwnershipTransferred(address(0), msg.sender);}
    function transferOwnership(address next) external onlyOwner {
        require(next != address(0), "zero");
        emit OwnershipTransferred(owner, next);
        owner = next;
    }
}

/// @title TriangleFlashV3
/// @notice Flash-borrow a token (e.g., USDC), swap through a V3 path (USDC->A->B->USDC),
///         and repay pool within the same transaction. Reverts if output < amount+fee.
contract TriangleFlashV3 is IUniswapV3FlashCallback, Ownable {
    error InvalidPool();
    error InsufficientProfit(uint256 out, uint256 owed);
    error ApproveFailed();
    error RouterCallFailed();
    error InvalidPathLength();
    error PathMustStartWithBorrowedToken();
    error PathMustEndWithBorrowedToken();

    /// @notice Immutable Uniswap V3 router
    ISwapRouterV3 public immutable router;

    /// @dev Data we pass to the callback to validate and execute
    struct FlashData {
        address pool;          // pool that called us (for validation)
        address tokenBorrow;   // token we borrowed (expected to be path start/end)
        uint256 amountBorrow;  // amount borrowed
        uint256 minOut;        // slippage minimum for the full path
        bytes path;            // encoded V3 path (must start AND end with tokenBorrow)
        address profitTo;      // where leftover profit is sent after repay (owner by default)
    }

    event TriangleExecuted(address indexed pool, address indexed tokenBorrow, uint256 amountIn, uint256 amountOut, uint256 amountOwed);

    constructor(address _router) {
        require(_router != address(0), "router=0");
        router = ISwapRouterV3(_router);
    }

    // ========= User entry =========

    /// @notice Execute a flash triangle in one tx.
    /// @param pool Uniswap V3 pool to flash from (must contain tokenBorrow as token0 or token1)
    /// @param tokenBorrow The token to borrow (e.g., USDC). path MUST start & end with this token.
    /// @param amountBorrow Amount to flash-borrow (in tokenBorrow units)
    /// @param path Encoded V3 path (tokenBorrow|fee|A|fee|B|fee|tokenBorrow)
    /// @param minOut Minimum acceptable amountOut from router (slippage control)
    /// @param profitTo Address to receive profit surplus after repay (if any)
    function executeTriangleFlash(
        address pool,
        address tokenBorrow,
        uint256 amountBorrow,
        bytes calldata path,
        uint256 minOut,
        address profitTo
    ) external onlyOwner {
        if (pool == address(0) || tokenBorrow == address(0)) revert InvalidPool();
        if (profitTo == address(0)) profitTo = owner;

        // Pool must contain tokenBorrow on either side
        address t0 = IUniswapV3Pool(pool).token0();
        address t1 = IUniswapV3Pool(pool).token1();
        require(t0 == tokenBorrow || t1 == tokenBorrow, "pool !contains tokenBorrow");

        // Path shape: 20 + 23*k (k >= 1) → at least 40 bytes total
        uint256 len = path.length;
        if (len < 40 || (len - 20) % 23 != 0) revert InvalidPathLength();

        // Decode first & last token DIRECTLY from calldata
        address start;
        address end;
        assembly {
            // first 20 bytes at path.offset
            start := shr(96, calldataload(path.offset))
            // last 20 bytes at path.offset + len - 20
            end := shr(96, calldataload(add(path.offset, sub(len, 20))))
        }

        if (start != tokenBorrow) revert PathMustStartWithBorrowedToken();
        // keep this only if you truly require round-trip back to tokenBorrow:
        if (end != tokenBorrow) revert PathMustEndWithBorrowedToken();

        // pack callback data (calldata → memory is OK; we already validated above)
        FlashData memory data = FlashData({
            pool: pool,
            tokenBorrow: tokenBorrow,
            amountBorrow: amountBorrow,
            minOut: minOut,
            path: path,
            profitTo: profitTo
        });

        // Borrow on the correct side
        if (t0 == tokenBorrow) {
            IUniswapV3Pool(pool).flash(address(this), amountBorrow, 0, abi.encode(data));
        } else {
            IUniswapV3Pool(pool).flash(address(this), 0, amountBorrow, abi.encode(data));
        }
    }

    // ========= Flash callback =========

    /// @notice Called by the pool after sending borrowed tokens. We must:
    ///         1) swap through Router using the provided path (exactInput)
    ///         2) repay amountBorrow + fee to the pool
    ///         3) keep any profit (if any) and send to profitTo
    error NotPool();
    error SwapFailed(bytes reason);

    event FlashStart(address pool, address token, uint256 amount);
    event TriangleExecuted(address indexed pool, address indexed tokenBorrow, uint256 amountIn, uint256 amountOut, uint256 amountOwed, uint256 profit);

    function uniswapV3FlashCallback(
        uint256 fee0,
        uint256 fee1,
        bytes calldata dataBytes
    ) external {
        FlashData memory data = abi.decode(dataBytes, (FlashData));
        address t0 = IUniswapV3Pool(data.pool).token0();
        address t1 = IUniswapV3Pool(data.pool).token1();
        bool borrowedIsToken0 = (t0 == data.tokenBorrow);
        uint256 fee = borrowedIsToken0 ? fee0 : fee1;
        uint256 amountOwed = data.amountBorrow + fee;

        // verify caller is the pool we flashed from
        if (msg.sender != data.pool) revert NotPool();

        emit FlashStart(data.pool, data.tokenBorrow, data.amountBorrow);

        // Approve router to spend the borrowed token
        if (!IERC20(data.tokenBorrow).approve(address(router), data.amountBorrow)) revert ApproveFailed();

        // Execute multi-hop swap: tokenBorrow -> A -> B -> tokenBorrow
        uint256 amountOut = router.exactInput(
            ISwapRouterV3.ExactInputParams({
                path: data.path,
                recipient: address(this),
                deadline: block.timestamp + 120,
                amountIn: data.amountBorrow,
                amountOutMinimum: data.minOut
            })
        );
        if (amountOut < amountOwed) revert InsufficientProfit(amountOut, amountOwed);

        IERC20(data.tokenBorrow).transfer(msg.sender, amountOwed);
        uint256 bal = IERC20(data.tokenBorrow).balanceOf(address(this));

        emit TriangleExecuted(data.pool, data.tokenBorrow, data.amountBorrow, amountOut, amountOwed);
    }
    // ========= Admin helpers =========

    /// @notice Rescue any ERC20 stuck on the contract
    function sweep(address token, address to, uint256 amount) external onlyOwner {
        require(IERC20(token).transfer(to, amount), "sweep failed");
    }
}"
    }
  },
  "settings": {
    "optimizer": {
      "enabled": false,
      "runs": 200
    },
    "outputSelection": {
      "*": {
        "*": [
          "evm.bytecode",
          "evm.deployedBytecode",
          "devdoc",
          "userdoc",
          "metadata",
          "abi"
        ]
      }
    },
    "remappings": []
  }
}}

Tags:
ERC20, DeFi, Swap, Factory|addr:0xb7d19be68ad4def2aa2ee7fe0c069837737f49f5|verified:true|block:23692277|tx:0x0c3cca21954504f65f970b49fc68ea979f8a981c683816cc46280a60a1ae420c|first_check:1761856041

Submitted on: 2025-10-30 21:27:24

Comments

Log in to comment.

No comments yet.