tycho-execution 0.386.1

Provides tools for encoding and executing swaps against Tycho router and protocol executors.
Documentation
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.26;

import {IExecutor} from "@interfaces/IExecutor.sol";
import {TransferManager} from "../TransferManager.sol";

error SkyExecutor__InvalidDataLength();
error SkyExecutor__InvalidTarget();
error SkyExecutor__InvalidDirection();
error SkyExecutor__TokenMismatch();

// Shared subset of DssLitePsm and UsdsPsmWrapper (the wrapper exposes the same
// compatibility getters; its dai() returns USDS).
interface IDssLitePsm {
    function dai() external view returns (address);
    function gem() external view returns (address);
    function tout() external view returns (uint256);
    function to18ConversionFactor() external view returns (uint256);
    function sellGem(address usr, uint256 gemAmt)
        external
        returns (uint256 daiOutWad);
    function buyGem(address usr, uint256 gemAmt)
        external
        returns (uint256 daiInWad);
}

interface IDaiUsds {
    function dai() external view returns (address);
    function usds() external view returns (address);
    function daiToUsds(address usr, uint256 wad) external;
    function usdsToDai(address usr, uint256 wad) external;
}

/// The three Sky venues and their tokens are fixed at deployment, so swap data
/// is just two bytes: a target selector and an `isGemToStable` direction flag
/// (mirroring tycho-simulation's SkyState semantics — the gem is USDC on the
/// PSM legs and USDS on the converter). No caller-controlled addresses.
contract SkyExecutor is IExecutor {
    uint256 private constant _WAD = 1e18;

    enum Target {
        Psm, // DssLitePsm: USDC (gem) <-> DAI
        Wrapper, // UsdsPsmWrapper: USDC (gem) <-> USDS
        Converter // DaiUsds: USDS (gem) <-> DAI
    }

    IDssLitePsm private immutable _psm;
    IDssLitePsm private immutable _wrapper;
    IDaiUsds private immutable _converter;
    address private immutable _dai;
    address private immutable _usdc;
    address private immutable _usds;
    uint256 private immutable _psmFactor;
    uint256 private immutable _wrapperFactor;

    constructor(address psm_, address wrapper_, address converter_) {
        _psm = IDssLitePsm(psm_);
        _wrapper = IDssLitePsm(wrapper_);
        _converter = IDaiUsds(converter_);
        _dai = _converter.dai();
        _usds = _converter.usds();
        _usdc = _psm.gem();
        _psmFactor = _psm.to18ConversionFactor();
        _wrapperFactor = _wrapper.to18ConversionFactor();
        // The three venues must agree on the token set they are wired for.
        if (
            _psm.dai() != _dai || _wrapper.gem() != _usdc
                || _wrapper.dai() != _usds
        ) {
            revert SkyExecutor__TokenMismatch();
        }
    }

    function fundsExpectedAddress(
        bytes calldata /* data */
    )
        external
        view
        returns (address receiver)
    {
        return msg.sender;
    }

    // slither-disable-next-line locked-ether
    function swap(uint256 amountIn, bytes calldata data, address receiver)
        external
        payable
    {
        (Target target, bool gemToStable) = _decodeData(data);

        if (target == Target.Converter) {
            if (gemToStable) {
                _converter.usdsToDai(receiver, amountIn);
            } else {
                _converter.daiToUsds(receiver, amountIn);
            }
            return;
        }

        (IDssLitePsm psm, uint256 factor) = target == Target.Psm
            ? (_psm, _psmFactor)
            : (_wrapper, _wrapperFactor);
        if (gemToStable) {
            // slither-disable-next-line unused-return
            psm.sellGem(receiver, amountIn);
        } else {
            // buyGem takes the gem OUTPUT amount and pulls the stable cost
            // (incl. tout) from the caller. Rounding down guarantees the cost
            // never exceeds amountIn; at most factor-1 wei of the stable stays
            // unspent at the router. A HALTED tout (uint256.max) reverts on
            // the checked addition, matching the venue's halt semantics.
            uint256 gemAmt = (amountIn * _WAD) / (factor * (_WAD + psm.tout()));
            // slither-disable-next-line unused-return
            psm.buyGem(receiver, gemAmt);
        }
    }

    function getTransferData(bytes calldata data)
        external
        view
        returns (
            TransferManager.TransferType transferType,
            address receiver,
            address tokenIn,
            address tokenOut,
            bool outputToRouter
        )
    {
        (Target target, bool gemToStable) = _decodeData(data);
        // All Sky venues pull tokenIn from the caller via allowance and honor a
        // recipient parameter for the output.
        transferType = TransferManager.TransferType.ProtocolWillDebit;
        outputToRouter = false;

        address stable;
        address gem;
        if (target == Target.Psm) {
            (stable, gem, receiver) = (_dai, _usdc, address(_psm));
        } else if (target == Target.Wrapper) {
            (stable, gem, receiver) = (_usds, _usdc, address(_wrapper));
        } else {
            (stable, gem, receiver) = (_dai, _usds, address(_converter));
        }
        (tokenIn, tokenOut) = gemToStable ? (gem, stable) : (stable, gem);
    }

    function _decodeData(bytes calldata data)
        internal
        pure
        returns (Target target, bool gemToStable)
    {
        if (data.length != 2) {
            revert SkyExecutor__InvalidDataLength();
        }
        uint8 rawTarget = uint8(data[0]);
        if (rawTarget > uint8(type(Target).max)) {
            revert SkyExecutor__InvalidTarget();
        }
        target = Target(rawTarget);
        uint8 rawDirection = uint8(data[1]);
        if (rawDirection > 1) {
            revert SkyExecutor__InvalidDirection();
        }
        gemToStable = rawDirection == 1;
    }
}