SynthetixDepositContract

Description:

Multi-signature wallet contract requiring multiple confirmations for transaction execution.

Blockchain: Ethereum

Source Code: View Code On The Blockchain

Solidity Source Code:

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

// ====================================================================
// |                       Imports                                  |
// ====================================================================

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { IAccessControl } from "@openzeppelin/contracts/access/IAccessControl.sol";
import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import { IERC1271 } from "@openzeppelin/contracts/interfaces/IERC1271.sol";
import { Math } from "@openzeppelin/contracts/utils/math/Math.sol";
import {
    AccessControlUpgradeable,
    AccessControlEnumerableUpgradeable
} from "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol";
import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol";
import { IPermit2, ISignatureTransfer } from "./interfaces/IPermit2.sol";
import { ISynthetixDepositContract } from "./interfaces/ISynthetixDepositContract.sol";
import { ReasonCodes } from "./libraries/ReasonCodes.sol";
import { AggregatorV3Interface } from "./interfaces/AggregatorV3Interface.sol";
import { CowOrder } from "./libraries/CowProtocol.sol";

/**
 * @title Synthetix Deposit Contract
 * @author 0xMithrandir
 * @notice This contract is the secure custody layer for the Synthetix perps exchange.
 * It handles all user deposits and manages a multi-stage, role-based withdrawal process.
 * @dev UUPS Upgradeable. All administrative actions MUST be controlled by a Timelock.
 */
contract SynthetixDepositContract is
    ISynthetixDepositContract,
    IERC1271,
    Initializable,
    UUPSUpgradeable,
    AccessControlEnumerableUpgradeable,
    ReentrancyGuardUpgradeable
{
    using SafeERC20 for IERC20;
    using CowOrder for CowOrder.Data;

    // ====================================================================
    // |                      Constants and Roles                       |
    // ====================================================================

    /// @notice Address of the Permit2 contract for gasless approvals
    address public constant PERMIT2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3;

    /// @notice CoW Protocol EIP-712 domain separator (Ethereum Mainnet)
    /// @dev This is the deterministic domain separator for GPv2Settlement on Ethereum mainnet.
    ///      Extracted from the `domainSeparator` view function at
    ///      https://etherscan.io/address/0x9008d19f58aabd9ed0d60971565aa8510560ab41#readContract#F2
    bytes32 private constant COW_DOMAIN_SEPARATOR = 0xc078f884a2676e1345748b1feace7b0abee5d00ecadb6e574dcdd109a63e8943;

    // Sepolia domain separator
    // bytes32 private constant COW_DOMAIN_SEPARATOR =
    // 0xdaee378bd0eb30ddf479272accf91761e697bc00e067a268f95f1d2732ed230b;

    /// @notice CoW Protocol VaultRelayer address (cross-chain deterministic)
    /// @dev The address from which CoW Protocol will attempt to retrieve tokens for order
    ///      settlement. This address is consistent across all chains where CoW Protocol is deployed.
    ///      This was extracted from the `vaultRelayer` view function at
    ///      https://etherscan.io/address/0x9008d19f58aabd9ed0d60971565aa8510560ab41#readContract#F7
    address private constant COW_VAULT_RELAYER = 0xC92E8bdf79f0507f65a392b0ab4667716BFE0110;

    /// @notice USDT token address on Ethereum mainnet (target settlement asset)
    address public constant USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7;
    // Sepolia USDT address
    // address public constant USDT = 0x58Eb19eF91e8A6327FEd391b51aE1887b833cc91;

    /// @notice USDT token decimals on Ethereum mainnet
    uint8 private constant USDT_DECIMALS = 6;

    /// @notice Role for relayers who approve/reject withdrawal requests
    bytes32 public constant RELAYER_ROLE = keccak256("RELAYER_ROLE");

    /// @notice Role for watchers who validate approved withdrawals
    bytes32 public constant WATCHER_ROLE = keccak256("WATCHER_ROLE");

    /// @notice Role for tellers who execute disbursements
    bytes32 public constant TELLER_ROLE = keccak256("TELLER_ROLE");

    /// @notice Role for guardians who can pause operations and resolve disputes
    bytes32 public constant GUARDIAN_ROLE = keccak256("GUARDIAN_ROLE");

    /**
     * @dev The OWNER_ROLE (also referred to as TIMELOCK_ADMIN_ROLE in spec) is the only role that can execute sensitive
     * changes.
     * It is intended to be held by a TimelockController contract to enforce a delay
     * on all critical administrative actions, mitigating governance attacks.
     */
    /// @notice Role for the timelock controller with highest privileges
    bytes32 public constant OWNER_ROLE = keccak256("OWNER_ROLE");

    /**
     * @dev The MANAGER_ROLE has similar authority to OWNER_ROLE but without timelock requirements.
     * Used for quick operational actions like revoking roles.
     */
    /// @notice Role for operational management without timelock
    bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE");

    /// @notice Role for authorized traders who can sign CoW swap orders
    bytes32 public constant AUTHORIZED_TRADER_ROLE = keccak256("AUTHORIZED_TRADER_ROLE");

    /// @notice ERC-1271 magic value for valid signatures
    bytes4 private constant EIP1271_MAGICVALUE = 0x1626ba7e;

    /// @notice Fixed point scale (1e18)
    uint256 private constant WAD = 1e18;

    /// @notice Minimum ABI-encoded envelope length (bytes):
    /// 12 head words (12*32) + bytes length (32) + padded 65-byte sig (96) = 512
    uint256 private constant MIN_ENVELOPE_LENGTH = 512;

    /// @notice Basis points denominator
    uint256 private constant BPS_DENOMINATOR = 10_000;

    // ====================================================================
    // |                        State Variables                         |
    // ====================================================================

    mapping(address collateralToken => CollateralConfig) private _collateralConfigs;
    mapping(uint256 requestId => WithdrawalRequest) private _withdrawalRequests;
    mapping(address user => uint256) private _userActiveWithdrawalId;
    mapping(uint256 requestId => mapping(address watcher => bool voted)) private _watcherVotes;
    mapping(address collateralToken => uint256) private _totalDeposited;
    mapping(address user => mapping(address collateralToken => int256 balance)) private _userBalance;
    mapping(address guardian => mapping(address collateralToken => uint256 approvalLimit)) private
        _guardianApprovalLimits;

    uint256 private _withdrawalRequestCounter;
    /// @notice Number of watcher votes required to validate a withdrawal
    uint256 public watcherQuorum;
    /// @notice Time in seconds after which a withdrawal request expires
    uint256 public withdrawalExpiryTimeout;

    // Granular pause states
    /// @notice Whether all deposits are paused globally
    bool public depositsGloballyPaused;
    /// @notice Whether all withdrawals are paused globally
    bool public withdrawalsGloballyPaused;
    /// @notice Per-token deposit pause status
    mapping(address => bool) public tokenDepositsPaused;
    /// @notice Per-token withdrawal pause status
    mapping(address => bool) public tokenWithdrawalsPaused;

    /// @notice Allowed slippage (in basis points) on the CoW order relative to oracle price
    /// Example: 500 = 5% tolerance. If zero, price checks are effectively disabled.
    uint256 public slippageToleranceBps;

    struct PriceFeedConfig {
        address aggregator;
        uint48 staleTimeout;
    }

    /// @notice Mapping of token => Chainlink USD price feed configuration
    mapping(address token => PriceFeedConfig) private _priceFeedsUSD;

    /// @notice Maximum allowed age for oracle data in seconds. Acts as default when no per-token timeout is set.
    uint256 public maxOracleStaleTimeout;

    // ====================================================================
    // |                         Custom Errors                          |
    // ====================================================================

    error ZeroAddress();
    error ZeroAmount();
    error TokenNotSupported();
    error InvalidPermit();
    error ActiveWithdrawalExists();
    error InvalidInput();
    error InvalidStateForAction(uint256 id, Status currentStatus);
    error AlreadyVoted(uint256 id, address voter);
    error AlreadyFinalized(uint256 id);
    error NotYourRequest(uint256 id, address caller);
    error RequestDoesNotExist(uint256 id);
    error EthDepositsNotSupported();
    error DepositsGloballyPaused();
    error WithdrawalsGloballyPaused();
    error TokenDepositsPaused(address token);
    error TokenWithdrawalsPaused(address token);
    error InsufficientContractBalance(address token, uint256 requested, uint256 available);
    error UnauthorizedRole();
    error AmountBelowMinimum(address token, uint256 amount, uint256 minimum);
    error ExceedsGlobalMaximum(address token, uint256 newTotal, uint256 maximum);
    error ExceedsUserMaximum(address token, uint256 newBalance, uint256 maximum);
    error GuardianApprovalLimitExceeded(address token, uint256 amount, uint256 limit);
    error InputArrayLengthMismatch();
    error ContractPaused();
    error UnauthorizedSigner();
    error InvalidEnvelope();
    error InvalidTradeParams();
    error InvalidTradeSignature();
    error InvalidReceiver(address receiver);
    error InvalidExpiry(uint256 validTo, uint256 currentTimestamp);
    error PriceFeedNotSet(address token);
    error InvalidOracleAnswer();
    error OraclePriceStale(address feed, uint256 updatedAt);

    // ====================================================================
    // |                   Initializer & Constructor                    |
    // ====================================================================

    /// @notice Constructor that disables initializers
    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        _disableInitializers();
    }

    /**
     * @notice Initializes the contract, setting up the Timelock as the sole administrator
     * @param _timelockAddress The address of the TimelockController that will govern this contract
     * @dev Sets up all role admins and default configurations
     */
    function initialize(address _timelockAddress) public initializer {
        __UUPSUpgradeable_init();
        __AccessControlEnumerable_init();
        __ReentrancyGuard_init();

        if (_timelockAddress == address(0)) revert ZeroAddress();

        // Grant OWNER_ROLE to the timelock address
        _grantRole(OWNER_ROLE, _timelockAddress);
        // Set OWNER_ROLE as the admin of itself and MANAGER_ROLE
        _setRoleAdmin(OWNER_ROLE, OWNER_ROLE);
        _setRoleAdmin(MANAGER_ROLE, OWNER_ROLE);
        // Set MANAGER_ROLE as the admin of operational roles
        _setRoleAdmin(RELAYER_ROLE, MANAGER_ROLE);
        _setRoleAdmin(WATCHER_ROLE, MANAGER_ROLE);
        _setRoleAdmin(TELLER_ROLE, MANAGER_ROLE);
        _setRoleAdmin(GUARDIAN_ROLE, MANAGER_ROLE);
        _setRoleAdmin(AUTHORIZED_TRADER_ROLE, MANAGER_ROLE);

        // Set default withdrawal expiry timeout to 5 minutes (300 seconds)
        withdrawalExpiryTimeout = 300;
        // Default slippage tolerance to 5% (500 bps)
        slippageToleranceBps = 500;
        // Default oracle staleness timeout to 1 day + 1 second (supports feeds with 1 day heartbeat)
        maxOracleStaleTimeout = 1 days + 1 seconds;
    }

    modifier whenNotPaused() {
        if (depositsGloballyPaused && withdrawalsGloballyPaused) {
            revert ContractPaused();
        }
        _;
    }

    // ====================================================================
    // |                       Read-Only Functions                      |
    // ====================================================================

    /// @inheritdoc ISynthetixDepositContract
    function isCollateralSupported(address _token) external view returns (bool) {
        return _collateralConfigs[_token].enabled;
    }

    /// @inheritdoc ISynthetixDepositContract
    function getCollateralConfig(address _token) external view returns (CollateralConfig memory) {
        return _collateralConfigs[_token];
    }

    /// @inheritdoc ISynthetixDepositContract
    function getTotalDeposited(address _token) external view returns (uint256) {
        return _totalDeposited[_token];
    }

    /// @inheritdoc ISynthetixDepositContract
    function priceFeedsUSD(address _token) public view override returns (address) {
        return _priceFeedsUSD[_token].aggregator;
    }

    /// @inheritdoc ISynthetixDepositContract
    function getUserBalance(address _user, address _token) external view returns (int256) {
        return _userBalance[_user][_token];
    }

    /// @inheritdoc ISynthetixDepositContract
    function getGuardianApprovalLimit(address _guardian, address _token) external view returns (uint256) {
        return _guardianApprovalLimits[_guardian][_token];
    }

    /// @inheritdoc ISynthetixDepositContract
    function getWithdrawalRequest(uint256 _id) external view returns (WithdrawalRequest memory) {
        return _withdrawalRequests[_id];
    }

    /// @inheritdoc ISynthetixDepositContract
    function getActiveWithdrawalId(address _user) external view returns (uint256) {
        return _userActiveWithdrawalId[_user];
    }

    /// @inheritdoc ISynthetixDepositContract
    function getWithdrawalRequestCounter() external view returns (uint256) {
        return _withdrawalRequestCounter;
    }

    /// @inheritdoc ISynthetixDepositContract
    function getAllWithdrawalRequests(
        uint256 _offset,
        uint256 _limit
    )
        external
        view
        returns (WithdrawalRequest[] memory requests, uint256[] memory ids, uint256 total)
    {
        total = _withdrawalRequestCounter;

        if (_offset >= total || _limit == 0) {
            return (new WithdrawalRequest[](0), new uint256[](0), total);
        }

        // Calculate actual return size
        uint256 returnSize = _limit;
        if (_offset + _limit > total) {
            returnSize = total - _offset;
        }

        requests = new WithdrawalRequest[](returnSize);
        ids = new uint256[](returnSize);

        // Direct access - O(limit) complexity instead of O(total)
        for (uint256 i = 0; i < returnSize; i++) {
            uint256 requestId = _offset + i + 1; // RequestIds start at 1
            requests[i] = _withdrawalRequests[requestId];
            ids[i] = requestId;
        }

        return (requests, ids, total);
    }

    /// @inheritdoc ISynthetixDepositContract
    function getWithdrawalRequestsByUser(
        address _user,
        uint256 _offset,
        uint256 _limit
    )
        external
        view
        returns (WithdrawalRequest[] memory requests, uint256[] memory ids, uint256 total)
    {
        total = _withdrawalRequestCounter;

        if (_offset >= total || _limit == 0) {
            return (new WithdrawalRequest[](0), new uint256[](0), total);
        }

        uint256 scanSize = _limit;
        if (_offset + _limit > total) {
            scanSize = total - _offset;
        }

        // scan to collect matching ids, then allocate exact-size arrays
        uint256[] memory matchedIds = new uint256[](scanSize);
        uint256 matchCount = 0;
        for (uint256 i = 0; i < scanSize; i++) {
            uint256 requestId = _offset + i + 1; // RequestIds start at 1
            if (_withdrawalRequests[requestId].user == _user) {
                matchedIds[matchCount] = requestId;
                unchecked {
                    ++matchCount;
                }
            }
        }

        requests = new WithdrawalRequest[](matchCount);
        ids = new uint256[](matchCount);
        for (uint256 i = 0; i < matchCount; i++) {
            uint256 requestId = matchedIds[i];
            ids[i] = requestId;
            requests[i] = _withdrawalRequests[requestId];
        }

        return (requests, ids, total);
    }

    // ====================================================================
    // |                     User-Facing Functions                      |
    // ====================================================================

    /// @inheritdoc ISynthetixDepositContract
    function deposit(DepositEntry[] calldata _deposits) external nonReentrant {
        if (depositsGloballyPaused) revert DepositsGloballyPaused();

        for (uint256 i = 0; i < _deposits.length; ++i) {
            DepositEntry calldata item = _deposits[i];
            if (item.token == address(0)) revert ZeroAddress();
            if (item.beneficiary == address(0)) revert ZeroAddress();
            if (item.amount == 0) revert ZeroAmount();

            CollateralConfig memory config = _collateralConfigs[item.token];
            if (!config.enabled) revert TokenNotSupported();
            if (tokenDepositsPaused[item.token]) {
                revert TokenDepositsPaused(item.token);
            }

            // Check deposit limits
            if (item.amount < config.userMinimum) {
                revert AmountBelowMinimum(item.token, item.amount, config.userMinimum);
            }

            uint256 newTotalDeposited = _totalDeposited[item.token] + item.amount;
            if (newTotalDeposited > config.globalMaximum) {
                revert ExceedsGlobalMaximum(item.token, newTotalDeposited, config.globalMaximum);
            }

            address beneficiary = item.beneficiary;
            int256 newUserBalance = _userBalance[beneficiary][item.token] + int256(item.amount);
            if (newUserBalance > int256(config.userMaximum)) {
                revert ExceedsUserMaximum(item.token, uint256(newUserBalance), config.userMaximum);
            }

            // Update tracking
            _totalDeposited[item.token] = newTotalDeposited;
            _userBalance[beneficiary][item.token] = newUserBalance;

            IERC20 token = IERC20(item.token);

            if (item.permitDetails.signature.length > 0) {
                ISignatureTransfer.PermitTransferFrom calldata p = item.permitDetails.permit;
                if (p.permitted.token != item.token || p.permitted.amount < item.amount || p.deadline < block.timestamp)
                {
                    revert InvalidPermit();
                }

                ISignatureTransfer.SignatureTransferDetails memory transferDetails =
                    ISignatureTransfer.SignatureTransferDetails({ to: address(this), requestedAmount: item.amount });

                // Execute the permit transfer with validated details
                IPermit2(PERMIT2).permitTransferFrom(p, transferDetails, msg.sender, item.permitDetails.signature);
            } else {
                token.safeTransferFrom(msg.sender, address(this), item.amount);
            }
            emit AssetDeposited(msg.sender, beneficiary, item.token, item.amount, item.subAccountId);
        }
    }

    /// @inheritdoc ISynthetixDepositContract
    function requestWithdrawal(WithdrawalEntry[] calldata _withdrawals) external onlyRole(RELAYER_ROLE) {
        if (withdrawalsGloballyPaused) revert WithdrawalsGloballyPaused();
        if (_withdrawals.length == 0) revert InvalidInput();

        for (uint256 i = 0; i < _withdrawals.length; ++i) {
            WithdrawalEntry calldata entry = _withdrawals[i];

            // Validate tokens and amounts arrays have same length
            if (entry.tokens.length != entry.amounts.length) revert InvalidInput();
            if (entry.tokens.length == 0) revert InvalidInput();
            if (entry.beneficiary == address(0)) revert ZeroAddress();

            // Check if user already has an active withdrawal
            if (_userActiveWithdrawalId[entry.beneficiary] != 0) {
                revert ActiveWithdrawalExists();
            }

            // Validate each token and amount
            for (uint256 j = 0; j < entry.tokens.length; ++j) {
                CollateralConfig storage config = _collateralConfigs[entry.tokens[j]];
                if (!config.enabled) {
                    revert TokenNotSupported();
                }
                if (entry.amounts[j] == 0) revert ZeroAmount();
                if (tokenWithdrawalsPaused[entry.tokens[j]]) {
                    revert TokenWithdrawalsPaused(entry.tokens[j]);
                }
                if (config.withdrawalMinimum > 0 && entry.amounts[j] < config.withdrawalMinimum) {
                    revert AmountBelowMinimum(entry.tokens[j], entry.amounts[j], config.withdrawalMinimum);
                }
                // Validate contract has sufficient balance
                uint256 contractBalance = IERC20(entry.tokens[j]).balanceOf(address(this));
                if (contractBalance < entry.amounts[j]) {
                    revert InsufficientContractBalance(entry.tokens[j], entry.amounts[j], contractBalance);
                }
            }

            // Create new withdrawal request
            uint256 newId;
            unchecked {
                newId = ++_withdrawalRequestCounter;
            }

            WithdrawalRequest storage newRequest = _withdrawalRequests[newId];
            newRequest.requestTime = uint32(block.timestamp);
            newRequest.user = entry.beneficiary;
            newRequest.status = Status.Requested;
            newRequest.reasonCode = ReasonCodes.REASON_NONE;
            newRequest.tokens = entry.tokens;
            newRequest.amounts = entry.amounts;

            _userActiveWithdrawalId[entry.beneficiary] = newId;

            emit WithdrawalRequested(newId, entry.beneficiary, entry.tokens, entry.amounts, block.timestamp);
        }
    }

    /// @inheritdoc ISynthetixDepositContract
    function cancelWithdrawal(uint256 _id) external whenNotPaused {
        WithdrawalRequest storage req = _withdrawalRequests[_id];
        if (req.user != msg.sender) revert NotYourRequest(_id, msg.sender);

        // Check if withdrawal is expired
        if (_isWithdrawalExpired(req)) {
            _finalizeRequest(_id, Status.Expired, ReasonCodes.REASON_EXPIRED);
            return;
        }

        Status currentStatus = req.status;
        if (currentStatus == Status.Validated || _isFinalStatus(currentStatus) || currentStatus == Status.Disputed) {
            revert InvalidStateForAction(_id, currentStatus);
        }

        _finalizeRequest(_id, Status.Cancelled, ReasonCodes.CANCELLED_BY_USER);
    }

    // ====================================================================
    // |             Role-Protected Functions (Withdrawal Lifecycle)            |
    // ====================================================================

    /// @inheritdoc ISynthetixDepositContract
    function rejectWithdrawals(
        uint256[] calldata _ids,
        uint256[] calldata _reasonCodes
    )
        external
        onlyRole(RELAYER_ROLE)
        whenNotPaused
    {
        if (_ids.length != _reasonCodes.length) {
            revert InputArrayLengthMismatch();
        }
        for (uint256 i = 0; i < _ids.length; ++i) {
            WithdrawalRequest storage req = _withdrawalRequests[_ids[i]];
            if (_isWithdrawalExpired(req)) {
                _finalizeRequest(_ids[i], Status.Expired, ReasonCodes.REASON_EXPIRED);
                continue;
            }
            if (req.status == Status.Requested) {
                _finalizeRequest(_ids[i], Status.Denied, _reasonCodes[i]);
            }
        }
    }

    /// @inheritdoc ISynthetixDepositContract
    function disputeWithdrawals(uint256[] calldata _ids, uint256[] calldata _reasonCodes) external whenNotPaused {
        if (!hasRole(RELAYER_ROLE, msg.sender) && !hasRole(WATCHER_ROLE, msg.sender)) {
            revert UnauthorizedRole();
        }
        if (_ids.length != _reasonCodes.length) {
            revert InputArrayLengthMismatch();
        }
        for (uint256 i = 0; i < _ids.length; ++i) {
            uint256 requestId = _ids[i];
            WithdrawalRequest storage req = _withdrawalRequests[requestId];
            if (req.user == address(0)) revert RequestDoesNotExist(requestId);

            if (_isWithdrawalExpired(req)) {
                _finalizeRequest(requestId, Status.Expired, ReasonCodes.REASON_EXPIRED);
                continue;
            }

            if (req.status == Status.Requested) {
                _updateStatus(requestId, Status.Requested, Status.Disputed, _reasonCodes[i]);
            }
        }
    }

    /// @inheritdoc ISynthetixDepositContract
    function castWatcherVotes(uint256[] calldata _requestIds) external onlyRole(WATCHER_ROLE) whenNotPaused {
        for (uint256 i = 0; i < _requestIds.length; ++i) {
            uint256 requestId = _requestIds[i];
            WithdrawalRequest storage req = _withdrawalRequests[requestId];

            if (req.user == address(0)) revert RequestDoesNotExist(requestId);

            if (_isWithdrawalExpired(req)) {
                _finalizeRequest(requestId, Status.Expired, ReasonCodes.REASON_EXPIRED);
                continue;
            }

            if (req.status == Status.Requested && !_watcherVotes[requestId][msg.sender]) {
                _watcherVotes[requestId][msg.sender] = true;
                req.watcherCount++;

                if (req.watcherCount >= watcherQuorum) {
                    _updateStatus(requestId, Status.Requested, Status.Validated, ReasonCodes.REASON_NONE);
                }
            }
        }
    }

    /// @inheritdoc ISynthetixDepositContract
    function disburseWithdrawals(uint256[] calldata _requestIds) external onlyRole(TELLER_ROLE) nonReentrant {
        if (withdrawalsGloballyPaused) revert WithdrawalsGloballyPaused();

        for (uint256 i = 0; i < _requestIds.length; ++i) {
            uint256 requestId = _requestIds[i];
            WithdrawalRequest storage req = _withdrawalRequests[requestId];

            if (req.status == Status.Validated && req.user != address(0)) {
                if (_isWithdrawalExpired(req)) {
                    _finalizeRequest(requestId, Status.Expired, ReasonCodes.REASON_EXPIRED);
                    continue;
                }

                address user = req.user;
                address[] memory tokens = req.tokens;
                uint256[] memory amounts = req.amounts;

                _finalizeRequest(requestId, Status.Disbursed, ReasonCodes.REASON_NONE);

                for (uint256 j = 0; j < tokens.length; ++j) {
                    if (tokenWithdrawalsPaused[tokens[j]]) {
                        revert TokenWithdrawalsPaused(tokens[j]);
                    }
                    if (amounts[j] > IERC20(tokens[j]).balanceOf(address(this))) {
                        revert InsufficientContractBalance(
                            tokens[j], amounts[j], IERC20(tokens[j]).balanceOf(address(this))
                        );
                    }
                    _userBalance[user][tokens[j]] -= int256(amounts[j]);
                    _decreaseTotalDeposited(tokens[j], amounts[j]);
                    IERC20(tokens[j]).safeTransfer(user, amounts[j]);
                }
            }
        }
    }

    /// @inheritdoc ISynthetixDepositContract
    function cancelStaleWithdrawals(uint256[] calldata _requestIds) external onlyRole(TELLER_ROLE) whenNotPaused {
        for (uint256 i = 0; i < _requestIds.length; ++i) {
            uint256 requestId = _requestIds[i];
            WithdrawalRequest storage req = _withdrawalRequests[requestId];

            if (_isWithdrawalExpired(req)) {
                _finalizeRequest(requestId, Status.Expired, ReasonCodes.REASON_EXPIRED);
            }
        }
    }

    /// @inheritdoc ISynthetixDepositContract
    function resolveDisputedWithdrawal(
        uint256 _requestId,
        bool _approve,
        uint256 _reasonCode
    )
        external
        onlyRole(GUARDIAN_ROLE)
        whenNotPaused
    {
        WithdrawalRequest storage req = _withdrawalRequests[_requestId];
        if (req.status != Status.Disputed) {
            revert InvalidStateForAction(_requestId, req.status);
        }

        if (_approve) {
            // Check guardian approval limits
            for (uint256 i = 0; i < req.tokens.length; ++i) {
                uint256 limit = _guardianApprovalLimits[msg.sender][req.tokens[i]];
                if (limit > 0 && req.amounts[i] > limit) {
                    revert GuardianApprovalLimitExceeded(req.tokens[i], req.amounts[i], limit);
                }
            }

            _updateStatus(_requestId, Status.Disputed, Status.Validated, ReasonCodes.REASON_MANUAL_OVERRIDE);
        } else {
            _finalizeRequest(_requestId, Status.Denied, _reasonCode);
        }
    }

    // ====================================================================
    // |                  Admin and Guardian Functions                    |
    // ====================================================================

    /// @inheritdoc ISynthetixDepositContract
    function addCollateral(address _token, CollateralConfig calldata _config) external onlyRole(OWNER_ROLE) {
        if (_token == address(0)) revert ZeroAddress();
        if (_config.globalMaximum == 0) revert InvalidInput();
        if (_config.userMaximum == 0) revert InvalidInput();
        if (_config.userMinimum > _config.userMaximum) revert InvalidInput();
        if (_config.withdrawalMinimum > _config.userMinimum) revert InvalidInput();
        if (_config.userMaximum > _config.globalMaximum) revert InvalidInput();

        _collateralConfigs[_token] = CollateralConfig({
            globalMaximum: _config.globalMaximum,
            userMinimum: _config.userMinimum,
            userMaximum: _config.userMaximum,
            withdrawalMinimum: _config.withdrawalMinimum,
            enabled: true
        });

        emit CollateralAdded(_token);
        emit CollateralConfigUpdated(
            _token, _config.globalMaximum, _config.userMinimum, _config.userMaximum, _config.withdrawalMinimum
        );
    }

    /// @inheritdoc ISynthetixDepositContract
    function updateCollateralConfig(address _token, CollateralConfig calldata _config) external onlyRole(OWNER_ROLE) {
        if (!_collateralConfigs[_token].enabled) revert TokenNotSupported();
        if (_config.globalMaximum == 0) revert InvalidInput();
        if (_config.userMaximum == 0) revert InvalidInput();
        if (_config.userMinimum > _config.userMaximum) revert InvalidInput();
        if (_config.withdrawalMinimum > _config.userMinimum) revert InvalidInput();
        if (_config.userMaximum > _config.globalMaximum) revert InvalidInput();

        _collateralConfigs[_token] = CollateralConfig({
            globalMaximum: _config.globalMaximum,
            userMinimum: _config.userMinimum,
            userMaximum: _config.userMaximum,
            withdrawalMinimum: _config.withdrawalMinimum,
            enabled: _config.enabled
        });

        emit CollateralConfigUpdated(
            _token, _config.globalMaximum, _config.userMinimum, _config.userMaximum, _config.withdrawalMinimum
        );
    }

    /// @inheritdoc ISynthetixDepositContract
    function removeCollateral(address _token) external onlyRole(OWNER_ROLE) {
        _collateralConfigs[_token].enabled = false;
        emit CollateralRemoved(_token);
    }

    /// @inheritdoc ISynthetixDepositContract
    function setGuardianApprovalLimits(
        address _guardian,
        GuardianApprovalLimit[] calldata _limits
    )
        external
        onlyRole(OWNER_ROLE)
    {
        if (_guardian == address(0)) revert ZeroAddress();

        for (uint256 i = 0; i < _limits.length; ++i) {
            _guardianApprovalLimits[_guardian][_limits[i].token] = _limits[i].limit;
            emit GuardianApprovalLimitSet(_guardian, _limits[i].token, _limits[i].limit);
        }
    }

    /// @inheritdoc ISynthetixDepositContract
    function setWatcherQuorum(uint256 _quorum) external onlyRole(OWNER_ROLE) {
        uint256 watcherCount = getRoleMemberCount(WATCHER_ROLE);
        if (_quorum > watcherCount || _quorum == 0) revert InvalidInput();

        watcherQuorum = _quorum;
        emit WatcherQuorumSet(_quorum);
    }

    /// @inheritdoc ISynthetixDepositContract
    function setWithdrawalExpiryTimeout(uint256 _timeout) external onlyRole(OWNER_ROLE) {
        if (_timeout < 60) {
            // Withdrawal expiry should be at least 1 minute to allow for processing time
            revert InvalidInput();
        }
        withdrawalExpiryTimeout = _timeout;
        emit WithdrawalExpiryTimeoutSet(_timeout);
    }

    /**
     * @notice Grants a role to an account
     * @param role The role to grant
     * @param account The account to grant the role to
     * @dev OWNER_ROLE and MANAGER_ROLE are restricted to OWNER_ROLE only
     *      Other roles can be granted by MANAGER_ROLE
     */
    function grantRole(bytes32 role, address account) public override(AccessControlUpgradeable, IAccessControl) {
        if (role == OWNER_ROLE || role == MANAGER_ROLE) {
            _checkRole(OWNER_ROLE);
        } else {
            if (!hasRole(OWNER_ROLE, _msgSender())) {
                _checkRole(getRoleAdmin(role));
            }
        }
        super._grantRole(role, account);
    }

    /**
     * @notice Revokes a role from an account
     * @param role The role to revoke
     * @param account The account to revoke the role from
     * @dev OWNER_ROLE changes require OWNER_ROLE (timelock)
     *  Other roles can be revoked by MANAGER_ROLE (no timelock)
     */
    function revokeRole(bytes32 role, address account) public override(AccessControlUpgradeable, IAccessControl) {
        if (role == OWNER_ROLE || role == MANAGER_ROLE) {
            _checkRole(OWNER_ROLE);
        } else {
            if (!hasRole(OWNER_ROLE, _msgSender())) {
                _checkRole(getRoleAdmin(role));
            }
        }
        super._revokeRole(role, account);
    }

    /// @inheritdoc ISynthetixDepositContract
    function pauseAllWithdrawals() external onlyRole(GUARDIAN_ROLE) {
        withdrawalsGloballyPaused = true;
        emit GlobalWithdrawalsPaused();
    }

    /// @inheritdoc ISynthetixDepositContract
    function pauseAllDeposits() external onlyRole(GUARDIAN_ROLE) {
        depositsGloballyPaused = true;
        emit GlobalDepositsPaused();
    }

    /// @inheritdoc ISynthetixDepositContract
    function pauseWithdrawalsFor(address token) external onlyRole(GUARDIAN_ROLE) {
        tokenWithdrawalsPaused[token] = true;
        emit TokenSpecificWithdrawalsPaused(token);
    }

    /// @inheritdoc ISynthetixDepositContract
    function pauseDepositsFor(address token) external onlyRole(GUARDIAN_ROLE) {
        tokenDepositsPaused[token] = true;
        emit TokenSpecificDepositsPaused(token);
    }

    /// @inheritdoc ISynthetixDepositContract
    function unpauseWithdrawals() external onlyRole(OWNER_ROLE) {
        withdrawalsGloballyPaused = false;
        emit GlobalWithdrawalsUnpaused();
    }

    /// @inheritdoc ISynthetixDepositContract
    function unpauseDeposits() external onlyRole(OWNER_ROLE) {
        depositsGloballyPaused = false;
        emit GlobalDepositsUnpaused();
    }

    /// @inheritdoc ISynthetixDepositContract
    function unpauseWithdrawalsFor(address token) external onlyRole(OWNER_ROLE) {
        tokenWithdrawalsPaused[token] = false;
        emit TokenSpecificWithdrawalsUnpaused(token);
    }

    /// @inheritdoc ISynthetixDepositContract
    function unpauseDepositsFor(address token) external onlyRole(OWNER_ROLE) {
        tokenDepositsPaused[token] = false;
        emit TokenSpecificDepositsUnpaused(token);
    }

    /// @inheritdoc ISynthetixDepositContract
    function overrideRequestStatus(
        uint256 _requestId,
        Status _newStatus,
        uint256 _reasonCode
    )
        external
        onlyRole(OWNER_ROLE)
    {
        WithdrawalRequest storage req = _withdrawalRequests[_requestId];
        Status currentStatus = req.status;

        // Prevent overriding already finalized requests unless forcing a different final state
        if (_isFinalStatus(currentStatus)) {
            if (!_isFinalStatus(_newStatus)) {
                revert InvalidStateForAction(_requestId, currentStatus);
            }
        }

        // Handle transitions to final states
        if (_isFinalStatus(_newStatus)) {
            // If already in a final state, just update the status directly
            if (_isFinalStatus(currentStatus)) {
                req.status = _newStatus;
                req.processedTime = uint32(block.timestamp);
                req.reasonCode = uint16(_reasonCode);
                emit WithdrawalStatusChanged(_requestId, req.user, _newStatus, _reasonCode);
            } else {
                _finalizeRequest(_requestId, _newStatus, _reasonCode);
            }
        } else {
            // For non-final states, just update the status
            _updateStatus(_requestId, currentStatus, _newStatus, _reasonCode);
        }
    }

    // ====================================================================
    // |                      CoW Protocol Integration                   |
    // ====================================================================

    /**
     * @notice Approve CoW VaultRelayer to spend collateral tokens
     * @param _token Token to approve
     * @param _amount Amount to approve
     * @dev Only callable by OWNER_ROLE (timelock)
     */
    function approveCowVaultRelayer(address _token, uint256 _amount) external onlyRole(OWNER_ROLE) {
        if (!_collateralConfigs[_token].enabled) revert TokenNotSupported();

        IERC20(_token).forceApprove(COW_VAULT_RELAYER, _amount);

        emit CowVaultRelayerApproved(_token, _amount);
    }

    /**
     * @notice Emergency function to revoke all approvals for a token
     * @param _token Token to revoke approvals for
     * @dev Only callable by GUARDIAN_ROLE for emergency response
     */
    function emergencyRevokeApprovals(address _token) external onlyRole(GUARDIAN_ROLE) {
        IERC20(_token).forceApprove(COW_VAULT_RELAYER, 0);

        emit CowApprovalRevoked(_token);
    }

    /**
     * @notice Check current VaultRelayer allowance for a token
     * @param _token Token to check
     * @return Current approval amount
     */
    function getCowAllowance(address _token) external view returns (uint256) {
        return IERC20(_token).allowance(address(this), COW_VAULT_RELAYER);
    }

    /**
     * @notice Sets the slippage tolerance (in basis points) for CoW validation
     * @param _bps Basis points (1% = 100 bps). Must be <= 10_000 (100%).
     */
    function setSlippageToleranceBps(uint256 _bps) external onlyRole(OWNER_ROLE) {
        if (_bps > 10_000) revert InvalidInput();
        slippageToleranceBps = _bps;
        emit SlippageToleranceSet(_bps);
    }

    /**
     * @notice Sets the Chainlink USD price feed for a token (including USDT)
     * @param _token The token address
     * @param _aggregator The Chainlink aggregator address returning token/USD (8 decimals typical)
     */
    function setPriceFeed(address _token, address _aggregator) external onlyRole(OWNER_ROLE) {
        if (_token == address(0) || _aggregator == address(0)) revert InvalidInput();
        _priceFeedsUSD[_token].aggregator = _aggregator;
        emit PriceFeedSet(_token, _aggregator);
    }

    /**
     * @notice Sets the maximum staleness for Chainlink oracle data
     * @param _timeout Seconds after which oracle price is considered stale. 0 disables the check.
     */
    function setOracleStaleTimeout(uint256 _timeout) external onlyRole(OWNER_ROLE) {
        maxOracleStaleTimeout = _timeout;
        emit OracleStaleTimeoutSet(address(0), _timeout);
    }

    /**
     * @notice Configures a token-specific oracle staleness timeout
     * @param _token Token address the timeout applies to
     * @param _timeout Seconds after which oracle price is considered stale for the token. 0 disables the check.
     */
    function setOracleStaleTimeoutForToken(address _token, uint256 _timeout) external onlyRole(OWNER_ROLE) {
        _priceFeedsUSD[_token].staleTimeout = uint48(_timeout);
        emit OracleStaleTimeoutSet(_token, _timeout);
    }

    /**
     * @notice ERC-1271 signature validation for CoW Protocol orders
     * @param orderDigest The hash of the CoW swap order
     * @param signature ABI-encoded envelope carrying order params and the EOA signature
     * @return The ERC-1271 magic value if signature is valid
     * @dev Accepts only ABI-encoded envelope signatures. Raw ECDSA signatures are not supported.
     */
    function isValidSignature(
        bytes32 orderDigest,
        bytes calldata signature
    )
        external
        view
        override(IERC1271, ISynthetixDepositContract)
        returns (bytes4)
    {
        // ABI-encoded envelope with order metadata and EOA signature:
        // (CowOrder.Data, bytes eoaSignature)
        if (signature.length < MIN_ENVELOPE_LENGTH) revert InvalidEnvelope();

        (CowOrder.Data memory order, bytes memory eoaSignature) = abi.decode(signature, (CowOrder.Data, bytes));

        // Order validations
        // Here we follow the same order as validating as presented in the CowOrder.Data struct.

        // Token checks: sellToken MUST be supported collateral; buyToken MUST be USDT only
        if (!_collateralConfigs[address(order.sellToken)].enabled) revert TokenNotSupported();
        if (address(order.buyToken) != USDT) revert InvalidTradeParams();

        // Receiver check
        // Here we ensure that the receiver is address(this) to make sure the trader can't
        // steal from us.
        if (order.receiver != address(this)) revert InvalidReceiver(order.receiver);

        // Token amount validations
        if (order.sellAmount == 0 || order.buyAmount == 0) revert InvalidTradeParams();

        // Price validation using Chainlink if tolerance is set
        if (slippageToleranceBps > 0) {
            _checkPriceSlippage(address(order.sellToken), order.sellAmount, order.buyAmount);
        }

        // We intentionally do **NOT** check:
        //  - `order.validTo` as the validity time is enforced by CoW Protocol
        //  - `order.appData` as we trust the EOA signer to popualte the metadata correctly
        //    CAUTION: The EOA would be able to define hooks that could grief gas consumption as well
        //             as define arbitrary referrers / partners that would reduce surplus payable to
        //             the implementing protocol (Synthetix).
        // - `order.feeAmount` as this is a deprecated field in CoW Protocol and must be set to `0`.
        // - `order.kind` as the EOA is entrusted with determing if it is:
        //    * bytes32(keccak256("sell")) - trade fixed sellAmount, receive *minimum* buyAmount
        //    * bytes32(keccak256("buy"))  - trade *maximum* sellAmount, received *fixed* buyAmount
        // - `order.partiallyFillable` is delegated to the EOA

        // Ensure sell and buy balances are traded against ERC20 balances (not balancer vault)
        if (order.sellTokenBalance != CowOrder.BALANCE_ERC20) revert InvalidTradeParams();
        if (order.buyTokenBalance != CowOrder.BALANCE_ERC20) revert InvalidTradeParams();

        // Verify that the `orderDigest` corresponds to the `order` that we have been checking
        if (order.hash(COW_DOMAIN_SEPARATOR) != orderDigest) revert InvalidTradeSignature();

        // Verify the embedded EOA signature corresponds to an authorized trader
        address signer = ECDSA.recover(orderDigest, eoaSignature);
        if (!hasRole(AUTHORIZED_TRADER_ROLE, signer)) {
            revert UnauthorizedSigner();
        }

        return EIP1271_MAGICVALUE;
    }

    // ====================================================================
    // |                          Oracle Helpers                         |
    // ====================================================================

    /**
     * @notice Fetches and validates the latest price from a Chainlink feed.
     * @dev It checks for a positive price, a complete round, and feed-specific price staleness.
     * @param feedAddress The address of the Chainlink AggregatorV3Interface contract.
     * @param staleTimeout Maximum permitted age for the oracle data (0 disables the check).
     * @return price The validated price answer from the oracle.
     * @return decimals The number of decimals the price is reported in.
     */
    function _getValidatedOraclePrice(
        address feedAddress,
        uint256 staleTimeout
    )
        internal
        view
        returns (int256 price, uint8 decimals)
    {
        (
            uint80 roundId,
            int256 answer,
            , // We don't need startedAt
            uint256 updatedAt,
            uint80 answeredInRound
        ) = AggregatorV3Interface(feedAddress).latestRoundData();

        // 1. Check for a valid, positive price
        if (answer <= 0) {
            revert InvalidOracleAnswer();
        }

        // 2. Check that the round is complete
        if (answeredInRound < roundId) {
            revert InvalidOracleAnswer(); // Or a more specific "IncompleteOracleRound" error
        }

        // 3. Check for stale data
        // The oracleStaleTimeout check prevents reverting if the feature is disabled (timeout = 0).
        if (staleTimeout > 0) {
            if (updatedAt == 0 || block.timestamp - updatedAt > staleTimeout) {
                revert OraclePriceStale(feedAddress, updatedAt);
            }
        }

        // 4. Return the validated data
        price = answer;
        decimals = AggregatorV3Interface(feedAddress).decimals();
    }

    /**
     * @notice Normalizes a value to 18 decimals (WAD representation).
     * @param value The raw value to normalize.
     * @param decimals The number of decimals of the raw value.
     * @return The value scaled to 1e18.
     */
    function _normalizeToWad(int256 value, uint8 decimals) internal pure returns (uint256) {
        if (decimals <= 18) {
            return uint256(value) * (10 ** (18 - decimals));
        } else {
            return uint256(value) / (10 ** (decimals - 18));
        }
    }

    /**
     * @notice Fetches and normalizes the USD prices for a given token and for USDT itself.
     * @param tokenFeed The Chainlink feed address for the token/USD pair.
     * @param tokenStaleTimeout Maximum allowed age for the token price data (0 disables the check).
     * @param usdtFeed The Chainlink feed address for the USDT/USD pair.
     * @param usdtStaleTimeout Maximum allowed age for the USDT price data (0 disables the check).
     * @return tokenUsd18 The token's price in USD, normalized to 18 decimals.
     * @return usdtUsd18 The USDT price in USD, normalized to 18 decimals.
     */
    function _getUsdPrices18(
        address tokenFeed,
        uint256 tokenStaleTimeout,
        address usdtFeed,
        uint256 usdtStaleTimeout
    )
        internal
        view
        returns (uint256 tokenUsd18, uint256 usdtUsd18)
    {
        // Get validated price data for the collateral token
        (int256 tokenAnswer, uint8 tokenDecimals) = _getValidatedOraclePrice(tokenFeed, tokenStaleTimeout);

        // Get validated price data for USDT
        (int256 usdtAnswer, uint8 usdtDecimals) = _getValidatedOraclePrice(usdtFeed, usdtStaleTimeout);

        // Normalize both prices to a common 18-decimal format
        tokenUsd18 = _normalizeToWad(tokenAnswer, tokenDecimals);
        usdtUsd18 = _normalizeToWad(usdtAnswer, usdtDecimals);
    }

    function _usdtPerTokenWad(address sellToken) internal view returns (uint256 usdtPerWad) {
        (address tokenFeed, uint256 tokenStaleTimeout) = _getPriceFeed(sellToken);
        (address usdtFeed, uint256 usdtStaleTimeout) = _getPriceFeed(USDT);

        (uint256 tokenUsd18, uint256 usdtUsd18) =
            _getUsdPrices18(tokenFeed, tokenStaleTimeout, usdtFeed, usdtStaleTimeout);
        // usdtPerToken scaled to WAD
        usdtPerWad = Math.mulDiv(tokenUsd18, WAD, usdtUsd18);
    }

    function _getPriceFeed(address token) private view returns (address feed, uint256 staleTimeout) {
        PriceFeedConfig storage config = _priceFeedsUSD[token];
        feed = config.aggregator;
        if (feed == address(0)) revert PriceFeedNotSet(token);

        staleTimeout = config.staleTimeout;
        if (staleTimeout == 0) {
            staleTimeout = maxOracleStaleTimeout;
        }
    }

    function _checkPriceSlippage(address sellToken, uint256 sellAmount, uint256 buyAmount) internal view {
        uint256 usdtPerTokenWad = _usdtPerTokenWad(sellToken);
        uint8 tokenDecimals = IERC20Metadata(sellToken).decimals();
        uint8 usdtTokenDecimals = USDT_DECIMALS;
        // expectedBuy in USDT smallest units
        uint256 expectedBuyWad = Math.mulDiv(sellAmount, usdtPerTokenWad, 10 ** tokenDecimals);
        uint256 expectedBuy = Math.mulDiv(expectedBuyWad, 10 ** usdtTokenDecimals, WAD);

        // Apply tolerance: min acceptable = expectedBuy * (1 - tol)
        uint256 minAcceptable = Math.mulDiv(expectedBuy, BPS_DENOMINATOR - slippageToleranceBps, BPS_DENOMINATOR);
        if (buyAmount < minAcceptable) revert InvalidTradeParams();
    }

    //@inheritdoc ISynthetixDepositContract
    function grantAuthorizedTraderRole(address _trader) external {
        grantRole(AUTHORIZED_TRADER_ROLE, _trader);
        emit AuthorizedTraderRoleGranted(_trader);
    }

    /// @inheritdoc ISynthetixDepositContract
    function revokeAuthorizedTraderRole(address _trader) external {
        revokeRole(AUTHORIZED_TRADER_ROLE, _trader);
        emit AuthorizedTraderRoleRevoked(_trader);
    }

    // ====================================================================
    // |                         Internal Logic                         |
    // ====================================================================

    /**
     * @notice Returns true if the provided status is a terminal state
     * @param _status The status to evaluate
     */
    function _isFinalStatus(Status _status) internal pure returns (bool) {
        return _status == Status.Disbursed || _status == Status.Denied || _status == Status.Cancelled
            || _status == Status.Expired;
    }

    /**
     * @notice Checks if a withdrawal request has expired
     * @param req The withdrawal request to check
     * @return True if the request has expired, false otherwise
     * @dev Requests expire if not processed within withdrawalExpiryTimeout
     */
    function _isWithdrawalExpired(WithdrawalRequest storage req) internal view returns (bool) {
        return (req.status == Status.Requested || req.status == Status.Validated)
            && block.timestamp > req.requestTime + withdrawalExpiryTimeout;
    }

    /**
     * @notice Decreases the tracked total deposits for a token without underflowing
     * @param _token The collateral token being debited
     * @param _amount The amount to subtract
     * @dev Saturates at zero so external balance adjustments (e.g. swaps) do not brick withdrawals
     */
    function _decreaseTotalDeposited(address _token, uint256 _amount) internal {
        uint256 current = _totalDeposited[_token];
        if (current <= _amount) {
            _totalDeposited[_token] = 0;
        } else {
            unchecked {
                _totalDeposited[_token] = current - _amount;
            }
        }
    }

    /**
     * @notice Updates the status of a withdrawal request with validation
     * @param _id The withdrawal request ID
     * @param _expected The expected current status
     * @param _new The new status to set
     * @param _reasonCode The reason for the status change
     * @dev Reverts if current status doesn't match expected
     */
    function _updateStatus(uint256 _id, Status _expected, Status _new, uint256 _reasonCode) internal {
        WithdrawalRequest storage req = _withdrawalRequests[_id];
        if (req.status != _expected) {
            revert InvalidStateForAction(_id, req.status);
        }
        req.status = _new;
        req.reasonCode = uint16(_reasonCode);
        emit WithdrawalStatusChanged(_id, req.user, _new, _reasonCode);
    }

    /**
     * @notice Finalizes a withdrawal request with a terminal status
     * @param _id The withdrawal request ID
     * @param _finalStatus The final status (Disbursed, Denied, Cancelled, or Expired)
     * @param _reasonCode The reason for the finalization
     * @dev Sets processedTime and clears user's active withdrawal
     */
    function _finalizeRequest(uint256 _id, Status _finalStatus, uint256 _reasonCode) internal {
        WithdrawalRequest storage req = _withdrawalRequests[_id];
        if (_isFinalStatus(req.status)) {
            revert AlreadyFinalized(_id);
        }
        req.status = _finalStatus;
        req.processedTime = uint32(block.timestamp);
        req.reasonCode = uint16(_reasonCode);
        _userActiveWithdrawalId[req.user] = 0;
        emit WithdrawalStatusChanged(_id, req.user, _finalStatus, _reasonCode);
    }

    // ====================================================================
    // |                       UUPS and Fallback                        |
    // ====================================================================

    /**
     * @notice Authorizes an upgrade to a new implementation
     * @param newImplementation The address of the new implementation contract
     * @dev Required by UUPSUpgradeable, restricted to OWNER_ROLE
     */
    function _authorizeUpgrade(address newImplementation) internal override onlyRole(OWNER_ROLE) {
        // Intentionally left empty as authorization is handled by role check
    }

    /**
     * @notice Prevents accidental ETH deposits
     * @dev Reverts any ETH sent directly to the contract
     */
    receive() external payable {
        revert EthDepositsNotSupported();
    }
}
"
    },
    "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.4.0) (token/ERC20/IERC20.sol)

pragma solidity >=0.4.16;

/**
 * @dev Interface of the ERC-20 standard as defined in the ERC.
 */
interface IERC20 {
    /**
     * @dev Emitted when `value` tokens are moved from one account (`from`) to
     * another (`to`).
     *
     * Note that `value` may be zero.
     */
    event Transfer(address indexed from, address indexed to, uint256 value);

    /**
     * @dev Emitted when the allowance of a `spender` for an `owner` is set by
     * a call to {approve}. `value` is the new allowance.
     */
    event Approval(address indexed owner, address indexed spender, uint256 value);

    /**
     * @dev Returns the value of tokens in existence.
     */
    function totalSupply() external view returns (uint256);

    /**
     * @dev Returns the value of tokens owned by `account`.
     */
    function balanceOf(address account) external view returns (uint256);

    /**
     * @dev Moves a `value` amount of tokens from the caller's account to `to`.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transfer(address to, uint256 value) external returns (bool);

    /**
     * @dev Returns the remaining number of tokens that `spender` will be
     * allowed to spend on behalf of `owner` through {transferFrom}. This is
     * zero by default.
     *
     * This value changes when {approve} or {transferFrom} are called.
     */
    function allowance(address owner, address spender) external view returns (uint256);

    /**
     * @dev Sets a `value` amount of tokens as the allowance of `spender` over the
     * caller's tokens.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * IMPORTANT: Beware that changing an allowance with this method brings the risk
     * that someone may use both the old and the new allowance by unfortunate
     * transaction ordering. One possible solution to mitigate this race
     * condition is to first reduce the spender's allowance to 0 and set the
     * desired value afterwards:
     * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
     *
     * Emits an {Approval} event.
     */
    function approve(address spender, uint256 value) external returns (bool);

    /**
     * @dev Moves a `value` amount of tokens from `from` to `to` using the
     * allowance mechanism. `value` is then deducted from the caller's
     * allowance.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transferFrom(address from, address to, uint256 value) external returns (bool);
}
"
    },
    "lib/openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.4.0) (token/ERC20/extensions/IERC20Metadata.sol)

pragma solidity >=0.6.2;

import {IERC20} from "../IERC20.sol";

/**
 * @dev Interface for the optional metadata functions from the ERC-20 standard.
 */
interface IERC20Metadata is IERC20 {
    /**
     * @dev Returns the name of the token.
     */
    function name() external view returns (string memory);

    /**
     * @dev Returns the symbol of the token.
     */
    function symbol() external view returns (string memory);

    /**
     * @dev Returns the decimals places of the token.
     */
    function decimals() external view returns (uint8);
}
"
    },
    "lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.3.0) (token/ERC20/utils/SafeERC20.sol)

pragma solidity ^0.8.20;

import {IERC20} from "../IERC20.sol";
import {IERC1363} from "../../../interfaces/IERC1363.sol";

/**
 * @title SafeERC20
 * @dev Wrappers around ERC-20 operations that throw on failure (when the token
 * contract returns false). Tokens that return no value (and instead revert or
 * throw on failure) are also supported, non-reverting calls are assumed to be
 * successful.
 * To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract,
 * which allows you to call the safe operations as `token.safeTransfer(...)`, etc.
 */
library SafeERC20 {
    /**
     * @dev An operation with an ERC-20 token failed.
     */
    error SafeERC20FailedOperation(address token);

    /**
     * @dev Indicates a failed `decreaseAllowance` request.
     */
    error SafeERC20FailedDecreaseAllowance(address spender, uint256 currentAllowance, uint256 requestedDecrease);

    /**
     * @dev Transfer `value` amount of `token` from the calling contract to `to`. If `token` returns no value,
     * non-reverting calls are assumed to be successful.
     */
    function safeTransfer(IERC20 token, address to, uint256 value) internal {
        if (!_safeTransfer(token, to, value, true)) {
            revert SafeERC20FailedOperation(address(token));
        }
    }

    /**
     * @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the
     * calling contract. If `token` returns no value, non-reverting calls are assumed to be successful.
     */
    function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {
        if (!_safeTransferFrom(token, from, to, value, true)) {
            revert SafeERC20FailedOperation(address(token));
        }
    }

    /**
     * @dev Variant of {safeTransfer} that returns a bool instead of reverting if the operation is not successful.
     */
    function trySafeTransfer(IERC20 token, address to, uint256 value) internal returns (bool) {
        return _safeTransfer(token, to, value, false);
    }

    /**
     * @dev Variant of {safeTransferFrom} that returns a bool instead of reverting if the operation is not successful.
     */
    function trySafeTransferFrom(IERC20 token, address from, address to, uint256 value) internal returns (bool) {
        return _safeTransferFrom(token, from, to, value, false);
    }

    /**
     * @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value,
     * non-reverting calls are assumed to be successful.
     *
     * IMPORTANT: If the token implements ERC-7674 (ERC-20 with temporary allowance), and if the "client"
     * smart contract uses ERC-7674 to set temporary allowances, then the "client" smart contract should avoid using
     * this function. Performing a {safeIncreaseAllowance} or {safeDecreaseAllowance} operation on a token contract
     * that has a non-zero temporary allowance (for that particular owner-spender) will result in unexpected behavior.
     */
    function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal {
        uint256 oldAllowance = token.allowance(address(this), spender);
        forceApprove(token, spender, oldAllowance + value);
    }

    /**
     * @dev Decrease the calling contract's allowance toward `spender` by `requestedDecrease`. If `token` returns no
     * value, non-reverting calls are assumed to be successful.
     *
     * IMPORTANT: If the token implements ERC-7674 (ERC-20 with temporary allowance), and if the "client"
     * smart contract uses ERC-7674 to set temporary allowances, then the "client" smart contract should avoid using
     * this function. Performing a {safeIncreaseAllowance} or {safeDecreaseAllowance} operation on a token contract
     * that has a non-zero temporary allowance (for that particular owner-spender) will result in unexpected behavior.
     */
    function safeDecreaseAllowance(IERC20 token, address spender, uint256 requestedDecrease) internal {
        unchecked {
            uint256 currentAllowance = token.allowance(address(this), spender);
            if (currentAllowance < requestedDecrease) {
                revert SafeERC20FailedDecreaseAllowance(spender, currentAllowance, requestedDecrease);
            }
            forceApprove(token, spender, currentAllowance - requestedDecrease);
        }
    }

    /**
     * @dev Set the calling contract's allowance toward `spender` to `value`. If `token` returns no value,
     * non-reverting calls are assumed to be successful. Meant to be used with tokens that require the approval
     * to be set to zero before setting it to a non-zero value, such as USDT.
     *
     * NOTE: If the token implements ERC-7674, this function will not modify any temporary allowance. This function
     * only sets the "standard" allowance. Any temporary allowance will remain active, in addition to the value being
     * set here.
     */
    function forceApprove(IERC20 token, address spender, uint256 value) internal {
        if (!_safeApprove(token, spender, value, false)) {
            if (!_safeApprove(token, spender, 0, true)) revert SafeERC20FailedOperation(address(token));
            if (!_safeApprove(token, spen

Tags:
ERC20, ERC165, Multisig, Swap, Voting, Timelock, Upgradeable, Multi-Signature, Factory, Oracle|addr:0x985814a3057cc631e145199db599adde77a7ca4b|verified:true|block:23739792|tx:0x36c3d381a76f748cc8db6d19864ca7d2d911116c5da48f5a33bd6c221b56d5b1|first_check:1762431337

Submitted on: 2025-11-06 13:15:40

Comments

Log in to comment.

No comments yet.