HIGH severity: ERC-7683 intents can never be filled because of an incorrectly encoded contract address. The Eco7683OriginSettler contract incorrectly sets the `FillInstruction.destinationSettler` field by using `bytes20(uint160(Eco7683DestinationSettler))`, which does not resolve to the correct contract address. This breaks the ERC-7683 compliance, as the destination settler address must point to a co...

Vulnerable code:
/* -*- c-basic-offset: 4 -*- */
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import {OnchainCrossChainOrder, ResolvedCrossChainOrder, GaslessCrossChainOrder, Output, FillInstruction} from "./types/ERC7683.sol";
import {IOriginSettler} from "./interfaces/ERC7683/IOriginSettler.sol";
import {Intent, Reward, Route, Call, TokenAmount} from "./types/Intent.sol";
import {OnchainCrosschainOrderData, GaslessCrosschainOrderData, ONCHAIN_CROSSCHAIN_ORDER_DATA_TYPEHASH, GASLESS_CROSSCHAIN_ORDER_DATA_TYPEHASH} from "./types/EcoERC7683.sol";
import {IntentSource} from "./IntentSource.sol";
import {Semver} from "./libs/Semver.sol";
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

/**
 * @title Eco7683OriginSettler
 * @notice Entry point to Eco Protocol via EIP-7683
 * @dev functionality is somewhat limited compared to interacting with Eco Protocol directly
 */
contract Eco7683OriginSettler is IOriginSettler, Semver, EIP712 {
    using ECDSA for bytes32;
    using SafeERC20 for IERC20;

    /// @notice typehash for gasless crosschain _order
    bytes32 public GASLESS_CROSSCHAIN_ORDER_TYPEHASH =
        keccak256(
            "GaslessCrossChainOrder(address originSettler,address user,uint256 nonce,uint256 originChainId,uint32 openDeadline,uint32 fillDeadline,bytes32 orderDataType,bytes32 orderDataHash)"
        );

    /// @notice address of IntentSource contract where intents are actually published
    address public immutable INTENT_SOURCE;

    /**
     * @notice Initializes the Eco7683OriginSettler
     * @param _name the name of the contract for EIP712
     * @param _version the version of the contract for EIP712
     * @param _intentSource the address of the IntentSource contract
     */
    constructor(
        string memory _name,
        string memory _version,
        address _intentSource
    ) EIP712(_name, _version) {
        INTENT_SOURCE = _intentSource;
    }

    /**
     * @notice Opens an Eco intent directly on chain
     * @dev to be called by the user
     * @dev assumes user has erc20 funds approved for the intent, and includes any reward native token in msg.value
     * @dev transfers the reward tokens at time of open
     * @param _order the OnchainCrossChainOrder that will be opened as an eco intent
     */
    function open(
        OnchainCrossChainOrder calldata _order
    ) external payable override {
        if (_order.orderDataType != ONCHAIN_CROSSCHAIN_ORDER_DATA_TYPEHASH) {
            revert TypeSignatureMismatch();
        }

        OnchainCrosschainOrderData memory onchainCrosschainOrderData = abi
            .decode(_order.orderData, (OnchainCrosschainOrderData));

        if (onchainCrosschainOrderData.route.source != block.chainid) {
            revert OriginChainIDMismatch();
        }

        Intent memory intent = Intent(
            onchainCrosschainOrderData.route,
            Reward(
                onchainCrosschainOrderData.creator,
                onchainCrosschainOrderData.prover,
                _order.fillDeadline,
                onchainCrosschainOrderData.nativeValue,
                onchainCrosschainOrderData.rewardTokens
            )
        );

        bytes32 orderId = _openEcoIntent(intent, msg.sender);

        emit Open(orderId, resolve(_order));
    }

    /**
     * @notice Opens an Eco intent on behalf of a user
     * @notice This method is made payable in the event that the caller of this method (a solver) wants to open
     * an intent that has native token as a reward. In this case, the solver would need to send the native
     * token as part of the transaction. How the intent's creator pays the solver is not covered by this method.
     * @dev to be called by the intent's solver
     * @dev assumes user has erc20 funds approved for the intent, and includes any reward native token in msg.value
     * @dev transfers the reward tokens at time of open
     * @param _order the GaslessCrossChainOrder that will be opened as an eco intent
     * @param _signature the signature of the user authorizing the intent to be opened
     * @param _originFillerData filler data for the origin chain (vestigial, not used)
     */
    function openFor(
        GaslessCrossChainOrder calldata _order,
        bytes calldata _signature,
        bytes calldata _originFillerData
    ) external payable override {
        if (block.timestamp > _order.openDeadline) {
            revert OpenDeadlinePassed();
        }
        if (!_verifyOpenFor(_order, _signature)) {
            revert BadSignature();
        }

        if (_order.orderDataType != GASLESS_CROSSCHAIN_ORDER_DATA_TYPEHASH) {
            revert TypeSignatureMismatch();
        }

        GaslessCrosschainOrderData memory gaslessCrosschainOrderData = abi
            .decode(_order.orderData, (GaslessCrosschainOrderData));

        if (_order.originChainId != block.chainid) {
            revert OriginChainIDMismatch();
        }

        Intent memory intent = Intent(
            Route(
                bytes32(_order.nonce),
                _order.originChainId,
                gaslessCrosschainOrderData.destination,
                gaslessCrosschainOrderData.inbox,
                gaslessCrosschainOrderData.routeTokens,
                gaslessCrosschainOrderData.calls
            ),
            Reward(
                _order.user,
                gaslessCrosschainOrderData.prover,
                _order.fillDeadline,
                gaslessCrosschainOrderData.nativeValue,
                gaslessCrosschainOrderData.rewardTokens
            )
        );

        bytes32 orderId = _openEcoIntent(intent, _order.user);

        emit Open(orderId, resolveFor(_order, _originFillerData));
    }

    /**
     * @notice resolves an OnchainCrossChainOrder to a ResolvedCrossChainOrder
     * @param _order the OnchainCrossChainOrder to be resolved
     */
    function resolve(
        OnchainCrossChainOrder calldata _order
    ) public view override returns (ResolvedCrossChainOrder memory) {
        OnchainCrosschainOrderData memory onchainCrosschainOrderData = abi
            .decode(_order.orderData, (OnchainCrosschainOrderData));
        uint256 routeTokenCount = onchainCrosschainOrderData
            .route
            .tokens
            .length;
        Output[] memory maxSpent = new Output[](routeTokenCount);
        for (uint256 i = 0; i < routeTokenCount; ++i) {
            TokenAmount memory approval = onchainCrosschainOrderData
                .route
                .tokens[i];
            maxSpent[i] = Output(
                bytes32(uint256(uint160(approval.token))),
                approval.amount,
                bytes32(uint256(uint160(address(0)))), //filler is not known
                onchainCrosschainOrderData.route.destination
            );
        }
        uint256 rewardTokenCount = onchainCrosschainOrderData
            .rewardTokens
            .length;
        Output[] memory minReceived = new Output[](
            rewardTokenCount +
                (onchainCrosschainOrderData.nativeValue > 0 ? 1 : 0)
        ); //rewards are fixed

        for (uint256 i = 0; i < rewardTokenCount; ++i) {
            minReceived[i] = Output(
                bytes32(
                    uint256(
                        uint160(
                            onchainCrosschainOrderData.rewardTokens[i].token
                        )
                    )
                ),
                onchainCrosschainOrderData.rewardTokens[i].amount,
                bytes32(uint256(uint160(address(0)))), //filler is not known
                onchainCrosschainOrderData.route.destination
            );
        }
        if (onchainCrosschainOrderData.nativeValue > 0) {
            minReceived[rewardTokenCount] = Output(
                bytes32(uint256(uint160(address(0)))),
                onchainCrosschainOrderData.nativeValue,
                bytes32(uint256(uint160(address(0)))),
                onchainCrosschainOrderData.route.destination
            );
        }

        Intent memory intent = Intent(
            onchainCrosschainOrderData.route,
            Reward(
                onchainCrosschainOrderData.creator,
                onchainCrosschainOrderData.prover,
                _order.fillDeadline,
                onchainCrosschainOrderData.nativeValue,
                onchainCrosschainOrderData.rewardTokens
            )
        );

        FillInstruction[] memory fillInstructions = new FillInstruction[](1);
        fillInstructions[0] = FillInstruction(
            uint64(onchainCrosschainOrderData.route.destination),
            bytes32(uint256(uint160(onchainCrosschainOrderData.route.inbox))),
            abi.encode(intent)
        );

        (bytes32 intentHash, , ) = IntentSource(INTENT_SOURCE).getIntentHash(
            intent
        );
        return
            ResolvedCrossChainOrder(
                onchainCrosschainOrderData.creator,
                onchainCrosschainOrderData.route.source,
                _order.fillDeadline,
                _order.fillDeadline,
                intentHash,
                maxSpent,
                minReceived,
                fillInstructions
            );
    }

    /**
     * @notice resolves GaslessCrossChainOrder to a ResolvedCrossChainOrder
     * @param _order the GaslessCrossChainOrder to be resolved
     * param _originFillerData filler data for the origin chain (not used)
     */
    function resolveFor(
        GaslessCrossChainOrder calldata _order,
        bytes calldata // _originFillerData keeping it for purpose of interface
    ) public view override returns (ResolvedCrossChainOrder memory) {
        GaslessCrosschainOrderData memory gaslessCrosschainOrderData = abi
            .decode(_order.orderData, (GaslessCrosschainOrderData));
        uint256 routeTokenCount = gaslessCrosschainOrderData.routeTokens.length;
        Output[] memory maxSpent = new Output[](routeTokenCount);
        for (uint256 i = 0; i < routeTokenCount; ++i) {
            TokenAmount memory requirement = gaslessCrosschainOrderData
                .routeTokens[i];
            maxSpent[i] = Output(
                bytes32(uint256(uint160(requirement.token))),
                requirement.amount,
                bytes32(uint256(uint160(address(0)))), //filler is not known
                gaslessCrosschainOrderData.destination
            );
        }
        uint256 rewardTokenCount = gaslessCrosschainOrderData
            .rewardTokens
            .length;
        Output[] memory minReceived = new Output[](
            rewardTokenCount +
                (gaslessCrosschainOrderData.nativeValue > 0 ? 1 : 0)
        ); //rewards are fixed

        for (uint256 i = 0; i < rewardTokenCount; ++i) {
            minReceived[i] = Output(
                bytes32(
                    uint256(
                        uint160(
                            gaslessCrosschainOrderData.rewardTokens[i].token
                        )
                    )
                ),
                gaslessCrosschainOrderData.rewardTokens[i].amount,
                bytes32(uint256(uint160(address(0)))), //filler is not known
                gaslessCrosschainOrderData.destination
            );
        }
        if (gaslessCrosschainOrderData.nativeValue > 0) {
            minReceived[rewardTokenCount] = Output(
                bytes32(uint256(uint160(address(0)))),
                gaslessCrosschainOrderData.nativeValue,
                bytes32(uint256(uint160(address(0)))),
                gaslessCrosschainOrderData.destination
            );
        }

        Intent memory intent = Intent(
            Route(
                bytes32(_order.nonce),
                _order.originChainId,
                gaslessCrosschainOrderData.destination,
                gaslessCrosschainOrderData.inbox,
                gaslessCrosschainOrderData.routeTokens,
                gaslessCrosschainOrderData.calls
            ),
            Reward(
                _order.user,
                gaslessCrosschainOrderData.prover,
                _order.fillDeadline,
                gaslessCrosschainOrderData.nativeValue,
                gaslessCrosschainOrderData.rewardTokens
            )
        );

        FillInstruction[] memory fillInstructions = new FillInstruction[](1);
        fillInstructions[0] = FillInstruction(
            uint64(gaslessCrosschainOrderData.destination),
            bytes32(uint256(uint160(gaslessCrosschainOrderData.inbox))),
            abi.encode(intent)
        );

        (bytes32 intentHash, , ) = IntentSource(INTENT_SOURCE).getIntentHash(
            intent
        );
        return
            ResolvedCrossChainOrder(
                _order.user,
                _order.originChainId,
                _order.openDeadline,
                _order.fillDeadline,
                intentHash,
                maxSpent,
                minReceived,
                fillInstructions
            );
    }

    /// @notice helper method for signature verification
    function _verifyOpenFor(
        GaslessCrossChainOrder calldata _order,
        bytes calldata _signature
    ) internal view returns (bool) {
        if (_order.originSettler != address(this)) {
            return false;
        }
        bytes32 structHash = keccak256(
            abi.encode(
                GASLESS_CROSSCHAIN_ORDER_TYPEHASH,
                _order.originSettler,
                _order.user,
                _order.nonce,
                _order.originChainId,
                _order.openDeadline,
                _order.fillDeadline,
                _order.orderDataType,
                keccak256(_order.orderData)
            )
        );

        bytes32 hash = _hashTypedDataV4(structHash);
        address signer = hash.recover(_signature);

        return signer == _order.user;
    }

    /// @notice helper method that actually opens the intent
    function _openEcoIntent(
        Intent memory _intent,
        address _user
    ) internal returns (bytes32 intentHash) {
        if (!IntentSource(INTENT_SOURCE).isIntentFunded(_intent)) {
            address vault = IntentSource(INTENT_SOURCE).intentVaultAddress(
                _intent
            );

            if (_intent.reward.nativeValue > 0) {
                if (msg.value < _intent.reward.nativeValue) {
                    revert InsufficientNativeReward();
                }

                payable(vault).transfer(_intent.reward.nativeValue);
            }
            uint256 rewardsLength = _intent.reward.tokens.length;
            for (uint256 i = 0; i < rewardsLength; ++i) {
                address token = _intent.reward.tokens[i].token;
                uint256 amount = _intent.reward.tokens[i].amount;

                IERC20(token).safeTransferFrom(_user, vault, amount);
            }
        }

        payable(msg.sender).transfer(address(this).balance);

        return IntentSource(INTENT_SOURCE).publish(_intent);
    }

    /// @notice EIP712 domain separator
    function domainSeparatorV4() public view returns (bytes32) {
        return _domainSeparatorV4();
    }
}


Vulnerability type: CWE-707|CWE-172