MEDIUM severity: Across bridging failure locks funds in Rebalancer contract. When Across bridging fails, funds are refunded to the Rebalancer contract, which is set as the depositor in depositV3Now. The root cause is the lack of a mechanism to rescue these refunded funds. Since the Rebalancer has no function to return these tokens to the market, they become permanently locke...

Vulnerable code:
// Copyright (c) 2025 Merge Layers Inc.
//
// This source code is licensed under the Business Source License 1.1
// (the "License"); you may not use this file except in compliance with the
// License. You may obtain a copy of the License at
//
//     https://github.com/malda-protocol/malda-lending/blob/main/LICENSE-BSL
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
// This file contains code derived from or inspired by Compound V2,
// originally licensed under the BSD 3-Clause License. See LICENSE-COMPOUND-V2
// for original license terms and attributions.

// SPDX-License-Identifier: AGPL-3.0
pragma solidity =0.8.28;

/*
 _____ _____ __    ____  _____ 
|     |  _  |  |  |    \|  _  |
| | | |     |  |__|  |  |     |
|_|_|_|__|__|_____|____/|__|__|   
*/

import {IRoles} from "src/interfaces/IRoles.sol";
import {IBridge} from "src/interfaces/IBridge.sol";
import {IOperator} from "src/interfaces/IOperator.sol";
import {ImTokenMinimal, ImToken} from "src/interfaces/ImToken.sol";
import {IRebalancer, IRebalanceMarket} from "src/interfaces/IRebalancer.sol";

import {SafeApprove} from "src/libraries/SafeApprove.sol";

contract Rebalancer is IRebalancer {
    // ----------- STORAGE ------------
    IRoles public roles;
    uint256 public nonce;
    mapping(uint32 => mapping(uint256 => Msg)) public logs;
    mapping(address => bool) public whitelistedBridges;
    mapping(uint32 => bool) public whitelistedDestinations;
    mapping(address => bool) public allowedList;

    address public saveAddress;

    struct TransferInfo {
        uint256 size;
        uint256 timestamp;
    }

    mapping(uint32 => mapping(address => uint256)) public maxTransferSizes;
    mapping(uint32 => mapping(address => uint256)) public minTransferSizes;
    mapping(uint32 => mapping(address => TransferInfo)) public currentTransferSize;
    uint256 public transferTimeWindow;

    constructor(address _roles, address _saveAddress) {
        require(_roles != address(0), Rebalancer_AddressNotValid());
        require(_saveAddress != address(0), Rebalancer_AddressNotValid());
        
        roles = IRoles(_roles);
        transferTimeWindow = 86400;
        saveAddress = _saveAddress;
    }

    // ----------- OWNER METHODS ------------
    function setAllowList(address[] calldata list, bool status) external {
        if (!roles.isAllowedFor(msg.sender, roles.GUARDIAN_BRIDGE())) revert Rebalancer_NotAuthorized();

        uint256 len = list.length;
        for (uint256 i; i < len; i++) {
            allowedList[list[i]] = status;
        }
        emit AllowedListUpdated(list, status);
    }

    function setWhitelistedBridgeStatus(address _bridge, bool _status) external {
        if (!roles.isAllowedFor(msg.sender, roles.GUARDIAN_BRIDGE())) revert Rebalancer_NotAuthorized();
        require(_bridge != address(0), Rebalancer_AddressNotValid());
        whitelistedBridges[_bridge] = _status;
        emit BridgeWhitelistedStatusUpdated(_bridge, _status);
    }

    function setWhitelistedDestination(uint32 _dstId, bool _status) external {
        if (!roles.isAllowedFor(msg.sender, roles.GUARDIAN_BRIDGE())) revert Rebalancer_NotAuthorized();
        emit DestinationWhitelistedStatusUpdated(_dstId, _status);
        whitelistedDestinations[_dstId] = _status;
    }

    function saveEth() external {
        if (!roles.isAllowedFor(msg.sender, roles.GUARDIAN_BRIDGE())) revert Rebalancer_NotAuthorized();

        uint256 amount = address(this).balance;
        // no need to check return value
        (bool success,) = saveAddress.call{value: amount}("");
        require(success, Rebalancer_RequestNotValid());
        emit EthSaved(amount);
    }

    function setMinTransferSize(uint32 _dstChainId, address _token, uint256 _limit) external {
        if (!roles.isAllowedFor(msg.sender, roles.GUARDIAN_BRIDGE())) revert Rebalancer_NotAuthorized();
        minTransferSizes[_dstChainId][_token] = _limit;
        emit MinTransferSizeUpdated(_dstChainId, _token, _limit);
    }

    function setMaxTransferSize(uint32 _dstChainId, address _token, uint256 _limit) external {
        if (!roles.isAllowedFor(msg.sender, roles.GUARDIAN_BRIDGE())) revert Rebalancer_NotAuthorized();
        maxTransferSizes[_dstChainId][_token] = _limit;
        emit MaxTransferSizeUpdated(_dstChainId, _token, _limit);
    }

    // ----------- VIEW METHODS ------------
    /**
     * @inheritdoc IRebalancer
     */
    function isBridgeWhitelisted(address bridge) external view returns (bool) {
        return whitelistedBridges[bridge];
    }

    /**
     * @inheritdoc IRebalancer
     */
    function isDestinationWhitelisted(uint32 dstId) external view returns (bool) {
        return whitelistedDestinations[dstId];
    }

    // ----------- EXTERNAL METHODS ------------
    /**
     * @inheritdoc IRebalancer
     */
    function sendMsg(address _bridge, address _market, uint256 _amount, Msg calldata _msg) external payable {
        // checks
        if (!roles.isAllowedFor(msg.sender, roles.REBALANCER_EOA())) revert Rebalancer_NotAuthorized();
        require(whitelistedBridges[_bridge], Rebalancer_BridgeNotWhitelisted());
        require(whitelistedDestinations[_msg.dstChainId], Rebalancer_DestinationNotWhitelisted());
        address _underlying = ImTokenMinimal(_market).underlying();
        require(_underlying == _msg.token, Rebalancer_RequestNotValid());

        // min transfer size check
        require(_amount > minTransferSizes[_msg.dstChainId][_msg.token], Rebalancer_TransferSizeMinNotMet());

        // max transfer size checks
        TransferInfo memory transferInfo = currentTransferSize[_msg.dstChainId][_msg.token];
        uint256 transferSizeDeadline = transferInfo.timestamp + transferTimeWindow;
        if (transferSizeDeadline < block.timestamp) {
            currentTransferSize[_msg.dstChainId][_msg.token] = TransferInfo(_amount, block.timestamp);
        } else {
            currentTransferSize[_msg.dstChainId][_msg.token].size += _amount;
        }

        uint256 _maxTransferSize = maxTransferSizes[_msg.dstChainId][_msg.token];
        if (_maxTransferSize > 0) {
            require(transferInfo.size + _amount < _maxTransferSize, Rebalancer_TransferSizeExcedeed());
        }

        // retrieve amounts (make sure to check min and max for that bridge)
        require(allowedList[_market], Rebalancer_MarketNotValid());
        IRebalanceMarket(_market).extractForRebalancing(_amount);

        // log
        unchecked {
            ++nonce;
        }
        logs[_msg.dstChainId][nonce] = _msg;

        // approve and trigger send
        SafeApprove.safeApprove(_msg.token, _bridge, _amount);
        IBridge(_bridge).sendMsg{value: msg.value}(
            _amount, _market, _msg.dstChainId, _msg.token, _msg.message, _msg.bridgeData
        );

        emit MsgSent(_bridge, _msg.dstChainId, _msg.token, _msg.message, _msg.bridgeData);
    }
}

// Copyright (c) 2025 Merge Layers Inc.
//
// This source code is licensed under the Business Source License 1.1
// (the "License"); you may not use this file except in compliance with the
// License. You may obtain a copy of the License at
//
//     https://github.com/malda-protocol/malda-lending/blob/main/LICENSE-BSL
//
// See the License for the specific language governing permissions and
// limitations under the License.

// SPDX-License-Identifier: AGPL-3.0
pragma solidity =0.8.28;

/*
 _____ _____ __    ____  _____ 
|     |  _  |  |  |    \|  _  |
| | | |     |  |__|  |  |     |
|_|_|_|__|__|_____|____/|__|__|   
*/

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

import {SafeApprove} from "src/libraries/SafeApprove.sol";

import {IBridge} from "src/interfaces/IBridge.sol";
import {ImTokenMinimal} from "src/interfaces/ImToken.sol";
import {IAcrossSpokePoolV3} from "src/interfaces/external/across/IAcrossSpokePoolV3.sol";

import {BaseBridge} from "src/rebalancer/bridges/BaseBridge.sol";

contract AccrossBridge is BaseBridge, IBridge, ReentrancyGuard {
    using SafeERC20 for IERC20;

    // ----------- STORAGE ------------
    address public immutable acrossSpokePool;
    uint256 public immutable maxSlippage;
    mapping(uint32 => mapping(address => bool)) public whitelistedRelayers;

    uint256 private constant SLIPPAGE_PRECISION = 1e5;

    struct DecodedMessage {
        uint256 inputAmount;
        uint256 outputAmount;
        address relayer;
        uint32 deadline;
        uint32 exclusivityDeadline;
    }

    // ----------- EVENTS ------------
    event Rebalanced(address indexed market, uint256 amount);
    event WhitelistedRelayerStatusUpdated(
        address indexed sender, uint32 indexed dstId, address indexed delegate, bool status
    );

    // ----------- ERRORS ------------
    error AcrossBridge_TokenMismatch();
    error AcrossBridge_NotAuthorized();
    error AcrossBridge_NotImplemented();
    error AcrossBridge_AddressNotValid();
    error AcrossBridge_SlippageNotValid();
    error AcrossBridge_RelayerNotValid();

    constructor(address _roles, address _spokePool) BaseBridge(_roles) {
        require(_spokePool != address(0), AcrossBridge_AddressNotValid());
        acrossSpokePool = _spokePool;
        maxSlippage = 1e4;
    }

    modifier onlySpokePool() {
        require(msg.sender == acrossSpokePool, AcrossBridge_NotAuthorized());
        _;
    }

    // ----------- OWNER ------------
    /**
     * @notice Whitelists a delegate address
     */
    function setWhitelistedRelayer(uint32 _dstId, address _relayer, bool status) external onlyBridgeConfigurator {
        whitelistedRelayers[_dstId][_relayer] = status;
        emit WhitelistedRelayerStatusUpdated(msg.sender, _dstId, _relayer, status);
    }

    // ----------- VIEW ------------
    /**
     * @inheritdoc IBridge
     */
    function getFee(uint32, bytes memory, bytes memory) external pure returns (uint256) {
        // need to use Across API
        revert AcrossBridge_NotImplemented();
    }

    /**
     * @notice returns if an address represents a whitelisted delegates
     */
    function isRelayerWhitelisted(uint32 dstChain, address relayer) external view returns (bool) {
        return whitelistedRelayers[dstChain][relayer];
    }

    // ----------- EXTERNAL ------------
    /**
     * @inheritdoc IBridge
     */
    function sendMsg(
        uint256 _extractedAmount,
        address _market,
        uint32 _dstChainId,
        address _token,
        bytes memory _message,
        bytes memory
    ) external payable onlyRebalancer {
        // decode message & checks
        DecodedMessage memory msgData = _decodeMessage(_message);
        require(_extractedAmount == msgData.inputAmount, BaseBridge_AmountMismatch());
        require(whitelistedRelayers[_dstChainId][msgData.relayer], AcrossBridge_RelayerNotValid());

        // retrieve tokens from `Rebalancer`
        IERC20(_token).safeTransferFrom(msg.sender, address(this), msgData.inputAmount);

        if (msgData.inputAmount > msgData.outputAmount) {
            uint256 maxSlippageInputAmount = msgData.inputAmount * maxSlippage / SLIPPAGE_PRECISION;
            require(
                msgData.inputAmount - msgData.outputAmount <= maxSlippageInputAmount, AcrossBridge_SlippageNotValid()
            );
        }

        // approve and send with Across
        _depositV3Now(_message, _token, _dstChainId, _market);
    }

    /**
     * @notice handles AcrossV3 SpokePool message
     * @param tokenSent the token address received
     * @param amount the token amount
     * @param message the custom message sent from source
     */
    function handleV3AcrossMessage(
        address tokenSent,
        uint256 amount,
        address, // relayer is unused
        bytes memory message
    ) external onlySpokePool nonReentrant {
        address market = abi.decode(message, (address));
        address _underlying = ImTokenMinimal(market).underlying();
        require(_underlying == tokenSent, AcrossBridge_TokenMismatch());
        if (amount > 0) {
            IERC20(tokenSent).safeTransfer(market, amount);
        }

        emit Rebalanced(market, amount);
    }

    // ----------- PRIVATE ------------
    function _decodeMessage(bytes memory _message) private pure returns (DecodedMessage memory) {
        (uint256 inputAmount, uint256 outputAmount, address relayer, uint32 deadline, uint32 exclusivityDeadline) =
            abi.decode(_message, (uint256, uint256, address, uint32, uint32));

        return DecodedMessage(inputAmount, outputAmount, relayer, deadline, exclusivityDeadline);
    }

    function _depositV3Now(bytes memory _message, address _token, uint32 _dstChainId, address _market) private {
        DecodedMessage memory msgData = _decodeMessage(_message);
        // approve and send with Across
        SafeApprove.safeApprove(_token, address(acrossSpokePool), msgData.inputAmount);
        IAcrossSpokePoolV3(acrossSpokePool).depositV3Now( // no need for `msg.value`; fee is taken from amount
            msg.sender, //depositor
            address(this), //recipient
            _token,
            address(0), //outputToken is automatically resolved to the same token on destination
            msgData.inputAmount,
            msgData.outputAmount, //outputAmount should be set as the inputAmount - relay fees; use Across API
            uint256(_dstChainId),
            msgData.relayer, //exclusiveRelayer
            msgData.deadline, //fillDeadline
            msgData.exclusivityDeadline, //can use Across API/suggested-fees or 0 to disable
            abi.encode(_market)
        );
    }
}


Vulnerability type: CWE-664|CWE-669