HIGH severity: Decimal truncation on asset vaults different to 18 decimals loss precission. The calculation of asset amounts in vaults with non-18 decimal precision leads to loss of precision due to inconsistent decimal scaling across contracts such as TellerWithMultiAssetSupport.sol, AccountantWithRateProviders.sol, and AtomicQueue.sol. This occurs because the code does not properly norma...

Vulnerable code:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.22;

import { ERC20 } from "@solmate/tokens/ERC20.sol";
import { WETH } from "@solmate/tokens/WETH.sol";
import { BoringVault } from "src/base/BoringVault.sol";
import { AccountantWithRateProviders } from "src/base/Roles/AccountantWithRateProviders.sol";
import { FixedPointMathLib } from "@solmate/utils/FixedPointMathLib.sol";
import { SafeTransferLib } from "@solmate/utils/SafeTransferLib.sol";
import { BeforeTransferHook } from "src/interfaces/BeforeTransferHook.sol";
import { Auth, Authority } from "@solmate/auth/Auth.sol";
import { ReentrancyGuard } from "@solmate/utils/ReentrancyGuard.sol";
import { IKeyring } from "src/interfaces/IKeyring.sol";
import { IRateProvider } from "src/interfaces/IRateProvider.sol";

/**
 * @title TellerWithMultiAssetSupport
 */
contract TellerWithMultiAssetSupport is Auth, BeforeTransferHook, ReentrancyGuard {
    using FixedPointMathLib for uint256;
    using SafeTransferLib for ERC20;
    using SafeTransferLib for WETH;

    // ========================================= CONSTANTS =========================================

    /**
     * @notice Native address used to tell the contract to handle native asset deposits.
     */
    address internal constant NATIVE = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;

    /**
     * @notice The maximum possible share lock period.
     */
    uint256 internal constant MAX_SHARE_LOCK_PERIOD = 3 days;

    // ========================================= STATE =========================================

    uint256 public depositCap;
    /**
     * @notice Mapping ERC20s to an isSupported bool.
     */
    mapping(ERC20 => bool) public isSupported;

    /**
     * @notice The deposit nonce used to map to a deposit hash.
     */
    uint96 public depositNonce = 1;

    /**
     * @notice After deposits, shares are locked to the msg.sender's address
     *         for `shareLockPeriod`.
     * @dev During this time all transfers from msg.sender will revert, and
     *      deposits are refundable.
     */
    uint64 public shareLockPeriod;

    /**
     * @notice Used to pause calls to `deposit` and `depositWithPermit`.
     */
    bool public isPaused;

    /**
     * @dev Maps deposit nonce to keccak256(address receiver, address _depositAsset, uint256 _depositAmount, uint256
     * _shareAmount, uint256 _timestamp, uint256 _shareLockPeriod).
     */
    mapping(uint256 => bytes32) public publicDepositHistory;

    /**
     * @notice Maps user address to the time their shares will be unlocked.
     */
    mapping(address => uint256) public shareUnlockTime;

    /**
     * @notice Access control mode for the vault
     */
    enum AccessControlMode {
        DISABLED,
        KEYRING_KYC,
        MANUAL_WHITELIST
    }

    /**
     * @notice Current access control mode
     */
    AccessControlMode public accessControlMode;

    /**
     * @notice Keyring contract interface
     */
    IKeyring public keyringContract;

    /**
     * @notice Keyring policy ID to check against
     */
    uint256 public keyringPolicyId;

    /**
     * @notice Manual whitelist for addresses when in MANUAL_WHITELIST mode
     */
    mapping(address => bool) public manualWhitelist;

    /**
     * @notice Whitelist for smart contracts (AMMs, protocols) that work in both modes
     */
    mapping(address => bool) public contractWhitelist;

    //============================== ERRORS ===============================

    error TellerWithMultiAssetSupport__ShareLockPeriodTooLong();
    error TellerWithMultiAssetSupport__SharesAreLocked();
    error TellerWithMultiAssetSupport__SharesAreUnLocked();
    error TellerWithMultiAssetSupport__BadDepositHash();
    error TellerWithMultiAssetSupport__AssetNotSupported();
    error TellerWithMultiAssetSupport__ZeroAssets();
    error TellerWithMultiAssetSupport__MinimumMintNotMet();
    error TellerWithMultiAssetSupport__MinimumAssetsNotMet();
    error TellerWithMultiAssetSupport__PermitFailedAndAllowanceTooLow();
    error TellerWithMultiAssetSupport__ZeroShares();
    error TellerWithMultiAssetSupport__Paused();
    error TellerWithMultiAssetSupport__KeyringCredentialInvalid();
    error TellerWithMultiAssetSupport__NotWhitelisted();

    //============================== EVENTS ===============================

    event Paused();
    event Unpaused();
    event AssetAdded(address indexed asset);
    event AssetRemoved(address indexed asset);
    event Deposit(
        uint256 indexed nonce,
        address indexed receiver,
        address indexed _depositAsset,
        uint256 _depositAmount,
        uint256 _shareAmount,
        uint256 depositTimestamp,
        uint256 shareLockPeriodAtTimeOfDeposit
    );
    event BulkDeposit(address indexed asset, uint256 _depositAmount);
    event BulkWithdraw(address indexed asset, uint256 _shareAmount);
    event DepositRefunded(uint256 indexed nonce, bytes32 depositHash, address indexed user);
    event DepositCapUpdated(uint256 oldCap, uint256 newCap);
    event AccessControlModeUpdated(AccessControlMode oldMode, AccessControlMode newMode);
    event KeyringConfigUpdated(address keyringContract, uint256 policyId);
    event ManualWhitelistUpdated(address indexed account, bool status);
    event ContractWhitelistUpdated(address indexed account, bool status);
    event ShareLockPeriodUpdated(uint64 oldPeriod, uint64 newPeriod);

    //============================== IMMUTABLES ===============================

    /**
     * @notice The BoringVault this contract is working with.
     */
    BoringVault public immutable vault;

    /**
     * @notice The AccountantWithRateProviders this contract is working with.
     */
    AccountantWithRateProviders public immutable accountant;

    /**
     * @notice One share of the BoringVault.
     */
    uint256 internal immutable ONE_SHARE;

    /**
     * @notice Check if an address has access to mint/redeem
     * @param _entity The address to check
     */
    modifier checkAccess(address _entity) {
        if (accessControlMode == AccessControlMode.KEYRING_KYC) {
            if (!contractWhitelist[_entity] && address(keyringContract) != address(0)) {
                if (!keyringContract.checkCredential(keyringPolicyId, _entity)) {
                    revert TellerWithMultiAssetSupport__KeyringCredentialInvalid();
                }
            }
        } else if (accessControlMode == AccessControlMode.MANUAL_WHITELIST) {
            if (!manualWhitelist[_entity] && !contractWhitelist[_entity]) {
                revert TellerWithMultiAssetSupport__NotWhitelisted();
            }
        }
        // If DISABLED, no checks performed
        _;
    }

    constructor(address _owner, address _vault, address _accountant) Auth(_owner, Authority(address(0))) {
        vault = BoringVault(payable(_vault));
        ONE_SHARE = 10 ** vault.decimals();
        accountant = AccountantWithRateProviders(_accountant);
    }

    // ========================================= ADMIN FUNCTIONS =========================================
    function setDepositCap(uint256 _depositCap) external requiresAuth {
        uint256 oldCap = depositCap;
        depositCap = _depositCap;
        emit DepositCapUpdated(oldCap, _depositCap);
    }

    /**
     * @notice Pause this contract, which prevents future calls to `deposit` and `depositWithPermit`.
     * @dev Callable by MULTISIG_ROLE.
     */
    function pause() external requiresAuth {
        isPaused = true;
        emit Paused();
    }

    /**
     * @notice Unpause this contract, which allows future calls to `deposit` and `depositWithPermit`.
     * @dev Callable by MULTISIG_ROLE.
     */
    function unpause() external requiresAuth {
        isPaused = false;
        emit Unpaused();
    }

    /**
     * @notice Adds this asset as a deposit asset.
     * @dev The accountant must also support pricing this asset, else the `deposit` call will revert.
     * @dev Callable by OWNER_ROLE.
     */
    function addAsset(ERC20 _asset) external requiresAuth {
        isSupported[_asset] = true;
        emit AssetAdded(address(_asset));
    }

    /**
     * @notice Removes this asset as a deposit asset.
     * @dev Callable by OWNER_ROLE.
     */
    function removeAsset(ERC20 _asset) external requiresAuth {
        isSupported[_asset] = false;
        emit AssetRemoved(address(_asset));
    }

    /**
     * @notice Sets the share lock period.
     * @dev This not only locks shares to the user address, but also serves as the pending deposit period, where
     * deposits can be reverted.
     * @dev If a new shorter share lock period is set, users with pending share locks could make a new deposit to
     * receive 1 wei shares,
     *      and have their shares unlock sooner than their original deposit allows. This state would allow for the user
     * deposit to be refunded,
     *      but only if they have not transferred their shares out of there wallet. This is an accepted limitation, and
     * should be known when decreasing
     *      the share lock period.
     * @dev Callable by OWNER_ROLE.
     */
    function setShareLockPeriod(uint64 _shareLockPeriod) external requiresAuth {
        if (_shareLockPeriod > MAX_SHARE_LOCK_PERIOD) revert TellerWithMultiAssetSupport__ShareLockPeriodTooLong();

        uint64 oldPeriod = shareLockPeriod;
        shareLockPeriod = _shareLockPeriod;

        emit ShareLockPeriodUpdated(oldPeriod, _shareLockPeriod);
    }

    /**
     * @notice Sets the access control mode
     * @dev Callable by OWNER_ROLE
     */
    function setAccessControlMode(AccessControlMode _mode) external requiresAuth {
        emit AccessControlModeUpdated(accessControlMode, _mode);
        accessControlMode = _mode;
    }

    /**
     * @notice Configure Keyring integration
     * @dev Callable by OWNER_ROLE
     */
    function setKeyringConfig(address _keyringContract, uint256 _policyId) external requiresAuth {
        keyringContract = IKeyring(_keyringContract);
        keyringPolicyId = _policyId;
        emit KeyringConfigUpdated(_keyringContract, _policyId);
    }

    /**
     * @notice Update manual whitelist
     * @dev Callable by OWNER_ROLE
     */
    function updateManualWhitelist(address[] calldata _addresses, bool _status) external requiresAuth {
        for (uint256 i = 0; i < _addresses.length; i++) {
            manualWhitelist[_addresses[i]] = _status;
            emit ManualWhitelistUpdated(_addresses[i], _status);
        }
    }

    /**
     * @notice Update contract whitelist (for AMMs, protocols)
     * @dev Callable by OWNER_ROLE
     */
    function updateContractWhitelist(address[] calldata _addresses, bool _status) external requiresAuth {
        for (uint256 i = 0; i < _addresses.length; i++) {
            contractWhitelist[_addresses[i]] = _status;
            emit ContractWhitelistUpdated(_addresses[i], _status);
        }
    }

    // ========================================= BeforeTransferHook FUNCTIONS =========================================

    /**
     * @notice Implement beforeTransfer hook to check if shares are locked.
     */
    function beforeTransfer(address _from) public view {
        if (shareUnlockTime[_from] > block.timestamp) revert TellerWithMultiAssetSupport__SharesAreLocked();
    }

    // ========================================= REVERT DEPOSIT FUNCTIONS =========================================

    /**
     * @notice Allows DEPOSIT_REFUNDER_ROLE to revert a pending deposit.
     * @dev Once a deposit share lock period has passed, it can no longer be reverted.
     * @dev It is possible the admin does not setup the BoringVault to call the transfer hook,
     *      but this contract can still be saving share lock state. In the event this happens
     *      deposits are still refundable if the user has not transferred their shares.
     *      But there is no guarantee that the user has not transferred their shares.
     * @dev Callable by STRATEGIST_MULTISIG_ROLE.
     */
    function refundDeposit(
        uint256 _nonce,
        address _receiver,
        address _depositAsset,
        uint256 _depositAmount,
        uint256 _shareAmount,
        uint256 _depositTimestamp,
        uint256 _shareLockUpPeriodAtTimeOfDeposit
    )
        external
        requiresAuth
    {
        if ((block.timestamp - _depositTimestamp) > _shareLockUpPeriodAtTimeOfDeposit) {
            // Shares are already unlocked, so we can not revert deposit.
            revert TellerWithMultiAssetSupport__SharesAreUnLocked();
        }
        bytes32 depositHash = keccak256(
            abi.encode(
                _receiver,
                _depositAsset,
                _depositAmount,
                _shareAmount,
                _depositTimestamp,
                _shareLockUpPeriodAtTimeOfDeposit
            )
        );
        if (publicDepositHistory[_nonce] != depositHash) revert TellerWithMultiAssetSupport__BadDepositHash();

        // Delete hash to prevent refund gas.
        delete publicDepositHistory[_nonce];

        accountant.checkpoint();

        // Burn shares and refund assets to receiver.
        vault.exit(_receiver, ERC20(_depositAsset), _depositAmount, _receiver, _shareAmount);

        emit DepositRefunded(_nonce, depositHash, _receiver);
    }

    // ========================================= USER FUNCTIONS =========================================

    /**
     * @notice Allows users to deposit into the BoringVault, if this contract is not paused.
     * @dev Publicly callable.
     */
    function deposit(
        ERC20 _depositAsset,
        uint256 _depositAmount,
        uint256 _minimumMint
    )
        external
        requiresAuth
        nonReentrant
        checkAccess(msg.sender)
        returns (uint256 shares)
    {
        if (isPaused) revert TellerWithMultiAssetSupport__Paused();
        if (!isSupported[_depositAsset]) revert TellerWithMultiAssetSupport__AssetNotSupported();

        shares = _erc20Deposit(_depositAsset, _depositAmount, _minimumMint, msg.sender);

        _afterPublicDeposit(msg.sender, _depositAsset, _depositAmount, shares, shareLockPeriod);
    }

    /**
     * @notice Allows users to deposit into BoringVault using permit.
     * @dev Publicly callable.
     */
    function depositWithPermit(
        ERC20 _depositAsset,
        uint256 _depositAmount,
        uint256 _minimumMint,
        uint256 _deadline,
        uint8 _v,
        bytes32 _r,
        bytes32 _s
    )
        external
        requiresAuth
        nonReentrant
        checkAccess(msg.sender)
        returns (uint256 shares)
    {
        if (isPaused) revert TellerWithMultiAssetSupport__Paused();
        if (!isSupported[_depositAsset]) revert TellerWithMultiAssetSupport__AssetNotSupported();

        // solhint-disable-next-line no-empty-blocks
        try _depositAsset.permit(msg.sender, address(vault), _depositAmount, _deadline, _v, _r, _s) { }
        catch {
            if (_depositAsset.allowance(msg.sender, address(vault)) < _depositAmount) {
                revert TellerWithMultiAssetSupport__PermitFailedAndAllowanceTooLow();
            }
        }
        shares = _erc20Deposit(_depositAsset, _depositAmount, _minimumMint, msg.sender);

        _afterPublicDeposit(msg.sender, _depositAsset, _depositAmount, shares, shareLockPeriod);
    }

    /**
     * @notice Allows on ramp role to deposit into this contract.
     * @dev Does NOT support native deposits.
     * @dev Callable by SOLVER_ROLE.
     */
    function bulkDeposit(
        ERC20 _depositAsset,
        uint256 _depositAmount,
        uint256 _minimumMint,
        address _to
    )
        external
        requiresAuth
        nonReentrant
        checkAccess(_to)
        returns (uint256 shares)
    {
        if (!isSupported[_depositAsset]) revert TellerWithMultiAssetSupport__AssetNotSupported();

        shares = _erc20Deposit(_depositAsset, _depositAmount, _minimumMint, _to);
        emit BulkDeposit(address(_depositAsset), _depositAmount);
    }

    /**
     * @notice Allows off ramp role to withdraw from this contract.
     * @dev Callable by SOLVER_ROLE.
     */
    function bulkWithdraw(
        ERC20 _withdrawAsset,
        uint256 _shareAmount,
        uint256 _minimumAssets,
        address _to
    )
        external
        requiresAuth
        checkAccess(msg.sender)
        returns (uint256 assetsOut)
    {
        if (!isSupported[_withdrawAsset]) revert TellerWithMultiAssetSupport__AssetNotSupported();
        if (_shareAmount == 0) revert TellerWithMultiAssetSupport__ZeroShares();

        accountant.checkpoint();

        // Get exchange rate in 18 decimals
        uint256 rate = accountant.getRate();

        // Calculate value in 18 decimals
        uint256 withdrawValueIn18 = _shareAmount.mulDivDown(rate, ONE_SHARE);

        // Convert to asset amount based on asset type
        if (address(_withdrawAsset) == address(accountant.base())) {
            // Base asset - convert from 18 to base decimals
            assetsOut = _changeDecimals(withdrawValueIn18, 18, accountant.decimals());
        } else {
            (bool isPegged,) = accountant.rateProviderData(_withdrawAsset);

            if (isPegged) {
                // Pegged asset - convert from 18 to asset decimals
                assetsOut = _changeDecimals(withdrawValueIn18, 18, _withdrawAsset.decimals());
            } else {
                // Non-pegged asset - use rate provider
                (, IRateProvider rateProvider) = accountant.rateProviderData(_withdrawAsset);
                uint256 assetRate = rateProvider.getRate();
                assetsOut = withdrawValueIn18.mulDivDown(10 ** _withdrawAsset.decimals(), assetRate);
            }
        }

        if (assetsOut < _minimumAssets) revert TellerWithMultiAssetSupport__MinimumAssetsNotMet();

        vault.exit(_to, _withdrawAsset, assetsOut, msg.sender, _shareAmount);
        emit BulkWithdraw(address(_withdrawAsset), _shareAmount);
    }

    // ========================================= INTERNAL HELPER FUNCTIONS =========================================

    /**
     * @notice Implements a common ERC20 deposit into BoringVault.
     */
    function _erc20Deposit(
        ERC20 _depositAsset,
        uint256 _depositAmount,
        uint256 _minimumMint,
        address _to
    )
        internal
        returns (uint256 shares)
    {
        if (_depositAmount == 0) revert TellerWithMultiAssetSupport__ZeroAssets();

        accountant.checkpoint();

        // Get exchange rate in 18 decimals
        uint256 rate = accountant.getRate();

        // Convert deposit amount to 18 decimal value based on asset type
        uint256 depositValueIn18;

        if (address(_depositAsset) == address(accountant.base())) {
            // Base asset - convert to 18 decimals
            depositValueIn18 = _changeDecimals(_depositAmount, accountant.decimals(), 18);
        } else {
            (bool isPegged,) = accountant.rateProviderData(_depositAsset);

            if (isPegged) {
                // Pegged asset - convert to 18 decimals (1:1 with base)
                depositValueIn18 = _changeDecimals(_depositAmount, _depositAsset.decimals(), 18);
            } else {
                // Non-pegged asset - use rate provider
                (, IRateProvider rateProvider) = accountant.rateProviderData(_depositAsset);
                uint256 assetRate = rateProvider.getRate();
                depositValueIn18 = _depositAmount.mulDivDown(assetRate, 10 ** _depositAsset.decimals());
            }
        }

        // Calculate shares using 18 decimal values
        shares = depositValueIn18.mulDivDown(ONE_SHARE, rate);

        if (shares < _minimumMint) revert TellerWithMultiAssetSupport__MinimumMintNotMet();

        uint256 shareValueInBase = shares.mulDivDown(rate, ONE_SHARE);
        // Convert to base decimals for cap check
        shareValueInBase = _changeDecimals(shareValueInBase, 18, accountant.decimals());
        uint256 currentTotalValue = vault.totalSupply().mulDivDown(rate, ONE_SHARE);
        currentTotalValue = _changeDecimals(currentTotalValue, 18, accountant.decimals());

        require(currentTotalValue + shareValueInBase <= depositCap, "Deposit cap exceeded");

        vault.enter(msg.sender, _depositAsset, _depositAmount, _to, shares);
    }

    /**
     * @notice Handle share lock logic, and event.
     */
    function _afterPublicDeposit(
        address _user,
        ERC20 _depositAsset,
        uint256 _depositAmount,
        uint256 _shares,
        uint256 _currentShareLockPeriod
    )
        internal
    {
        shareUnlockTime[_user] = block.timestamp + _currentShareLockPeriod;

        uint256 nonce = depositNonce;
        publicDepositHistory[nonce] = keccak256(
            abi.encode(_user, _depositAsset, _depositAmount, _shares, block.timestamp, _currentShareLockPeriod)
        );
        depositNonce++;
        emit Deposit(
            nonce, _user, address(_depositAsset), _depositAmount, _shares, block.timestamp, _currentShareLockPeriod
        );
    }

    /**
     * @notice Internal helper to change decimals
     */
    function _changeDecimals(uint256 amount, uint8 fromDecimals, uint8 toDecimals) internal pure returns (uint256) {
        if (fromDecimals == toDecimals) return amount;
        if (fromDecimals < toDecimals) {
            return amount * 10 ** (toDecimals - fromDecimals);
        } else {
            return amount / 10 ** (fromDecimals - toDecimals);
        }
    }
}

// SPDX-License-Identifier: MIT
pragma solidity 0.8.22;

import { FixedPointMathLib } from "@solmate/utils/FixedPointMathLib.sol";
import { IRateProvider } from "src/interfaces/IRateProvider.sol";
import { ERC20 } from "@solmate/tokens/ERC20.sol";
import { SafeTransferLib } from "@solmate/utils/SafeTransferLib.sol";
import { BoringVault } from "src/base/BoringVault.sol";
import { Auth, Authority } from "@solmate/auth/Auth.sol";

/**
 * @title AccountantWithRateProviders
 */
contract AccountantWithRateProviders is Auth, IRateProvider {
    using FixedPointMathLib for uint256;
    using SafeTransferLib for ERC20;

    // ========================================= STRUCTS =========================================

    /**
     * @param payoutAddress the address `claimFees` sends fees to
     * @param feesOwedInBase total pending fees owed in terms of base
     * @param totalSharesLastUpdate total amount of shares the last exchange rate update
     * @param exchangeRate the current exchange rate in terms of base
     * @param _allowedExchangeRateChangeUpper the max allowed change to exchange rate from an update
     * @param _allowedExchangeRateChangeLower the min allowed change to exchange rate from an update
     * @param _lastUpdateTimestamp the block timestamp of the last exchange rate update
     * @param _isPaused whether or not this contract is paused
     * @param _minimumUpdateDelayInSeconds the minimum amount of time that must pass between
     *        exchange rate updates, such that the update won't trigger the contract to be paused
     * @param _managementFee the management fee
     */
    struct AccountantState {
        address _payoutAddress;
        uint128 _feesOwedInBase;
        uint128 _totalSharesLastUpdate;
        uint96 _exchangeRate;
        uint16 _allowedExchangeRateChangeUpper;
        uint16 _allowedExchangeRateChangeLower;
        uint64 _lastUpdateTimestamp;
        bool _isPaused;
        uint32 _minimumUpdateDelayInSeconds;
        uint16 _managementFee;
    }

    /**
     * @notice Lending specific state
     * @param _lendingRate Annual lending interest rate in basis points (1000 = 10%)
     * @param _lastAccrualTime Timestamp of last interest accrual
     */
    struct LendingInfo {
        uint256 _lendingRate; // Rate for vault growth
        uint256 _lastAccrualTime; // Last checkpoint
    }

    /**
     * @param isPeggedToBase whether or not the asset is 1:1 with the base asset
     * @param rateProvider the rate provider for this asset if `isPeggedToBase` is false
     */
    struct RateProviderData {
        bool isPeggedToBase;
        IRateProvider rateProvider;
    }

    // ========================================= CONSTANTS =========================================
    // Constants for calculations
    uint256 constant SECONDS_PER_YEAR = 365 days;
    uint256 constant BASIS_POINTS = 10_000;

    // ========================================= STATE =========================================

    /**
     * @notice Store the accountant state in 3 packed slots.
     */
    AccountantState public accountantState;
    LendingInfo public lendingInfo;
    uint256 public maxLendingRate;

    /**
     * @notice Maps ERC20s to their RateProviderData.
     */
    mapping(ERC20 => RateProviderData) public rateProviderData;

    //============================== ERRORS ===============================

    error AccountantWithRateProviders__UpperBoundTooSmall();
    error AccountantWithRateProviders__LowerBoundTooLarge();
    error AccountantWithRateProviders__ManagementFeeTooLarge();
    error AccountantWithRateProviders__Paused();
    error AccountantWithRateProviders__ZeroFeesOwed();
    error AccountantWithRateProviders__OnlyCallableByBoringVault();
    error AccountantWithRateProviders__UpdateDelayTooLarge();
    error AccountantWithRateProviders__UpperMustExceedLower();

    //============================== EVENTS ===============================

    event Paused();
    event Unpaused();
    event DelayInSecondsUpdated(uint32 oldDelay, uint32 newDelay);
    event UpperBoundUpdated(uint16 oldBound, uint16 newBound);
    event LowerBoundUpdated(uint16 oldBound, uint16 newBound);
    event PayoutAddressUpdated(address oldPayout, address newPayout);
    event RateProviderUpdated(address asset, bool isPegged, address rateProvider);
    event ExchangeRateUpdated(uint96 oldRate, uint96 newRate, uint64 currentTime);
    event FeesClaimed(address indexed feeAsset, uint256 amount);
    event LendingRateUpdated(uint256 newRate, uint256 timestamp);
    event ManagementFeeRateUpdated(uint16 newRate, uint256 timestamp);
    event MaxLendingRateUpdated(uint256 newMaxRate);
    event Checkpoint(uint256 indexed timestamp);

    //============================== IMMUTABLES ===============================

    /**
     * @notice The base asset rates are provided in.
     */
    ERC20 public immutable base;

    /**
     * @notice The decimals rates are provided in.
     */
    uint8 public immutable decimals;

    /**
     * @notice The BoringVault this accountant is working with.
     *         Used to determine share supply for fee calculation.
     */
    BoringVault public immutable vault;

    /**
     * @notice One share of the BoringVault.
     */
    uint256 internal immutable ONE_SHARE;

    constructor(
        address _owner,
        address _vault,
        address _payoutAddress,
        uint96 _startingExchangeRate,
        address _base,
        uint16 _allowedExchangeRateChangeUpper,
        uint16 _allowedExchangeRateChangeLower,
        uint32 _minimumUpdateDelayInSeconds,
        uint16 _managementFee
    )
        Auth(_owner, Authority(address(0)))
    {
        base = ERC20(_base);
        decimals = ERC20(_base).decimals();
        vault = BoringVault(payable(_vault));
        ONE_SHARE = 10 ** vault.decimals();
        accountantState = AccountantState({
            _payoutAddress: _payoutAddress,
            _feesOwedInBase: 0,
            _totalSharesLastUpdate: uint128(vault.totalSupply()),
            _exchangeRate: _startingExchangeRate,
            _allowedExchangeRateChangeUpper: _allowedExchangeRateChangeUpper,
            _allowedExchangeRateChangeLower: _allowedExchangeRateChangeLower,
            _lastUpdateTimestamp: uint64(block.timestamp),
            _isPaused: false,
            _minimumUpdateDelayInSeconds: _minimumUpdateDelayInSeconds,
            _managementFee: _managementFee
        });
        lendingInfo._lastAccrualTime = block.timestamp;
        maxLendingRate = 5000;
    }

    // ========================================= ADMIN FUNCTIONS =========================================
    /**
     * @notice Pause this contract, which prevents future calls to `updateExchangeRate`, and any safe rate
     *         calls will revert.
     * @dev Pausing only prevents state changes, not time-based calculations
     * @dev Callable by MULTISIG_ROLE.
     */
    function pause() public requiresAuth {
        accountantState._isPaused = true;
        emit Paused();
    }

    /**
     * @notice Unpause this contract, which allows future calls to `updateExchangeRate`, and any safe rate
     *         calls will stop reverting.
     * @dev Callable by MULTISIG_ROLE.
     */
    function unpause() external requiresAuth {
        accountantState._isPaused = false;
        emit Unpaused();
    }

    /**
     * @notice Update the minimum time delay between `updateExchangeRate` calls.
     * @dev There are no input requirements, as it is possible the admin would want
     *      the exchange rate updated as frequently as needed.
     * @dev Callable by OWNER_ROLE.
     */
    function updateDelay(uint32 _minimumUpdateDelayInSeconds) external requiresAuth {
        if (_minimumUpdateDelayInSeconds > 14 days) revert AccountantWithRateProviders__UpdateDelayTooLarge();
        uint32 oldDelay = accountantState._minimumUpdateDelayInSeconds;
        accountantState._minimumUpdateDelayInSeconds = _minimumUpdateDelayInSeconds;
        emit DelayInSecondsUpdated(oldDelay, _minimumUpdateDelayInSeconds);
    }

    /**
     * @notice Update the allowed upper bound change of exchange rate between `updateExchangeRateCalls`.
     * @dev Callable by OWNER_ROLE.
     */
    function updateUpper(uint16 _allowedExchangeRateChangeUpper) external requiresAuth {
        if (_allowedExchangeRateChangeUpper <= accountantState._allowedExchangeRateChangeLower) {
            revert AccountantWithRateProviders__UpperMustExceedLower();
        }
        if (_allowedExchangeRateChangeUpper < BASIS_POINTS) revert AccountantWithRateProviders__UpperBoundTooSmall();
        uint16 oldBound = accountantState._allowedExchangeRateChangeUpper;
        accountantState._allowedExchangeRateChangeUpper = _allowedExchangeRateChangeUpper;
        emit UpperBoundUpdated(oldBound, _allowedExchangeRateChangeUpper);
    }

    /**
     * @notice Update the allowed lower bound change of exchange rate between `updateExchangeRateCalls`.
     * @dev Callable by OWNER_ROLE.
     */
    function updateLower(uint16 _allowedExchangeRateChangeLower) external requiresAuth {
        if (_allowedExchangeRateChangeLower >= accountantState._allowedExchangeRateChangeUpper) {
            revert AccountantWithRateProviders__UpperMustExceedLower();
        }
        if (_allowedExchangeRateChangeLower > BASIS_POINTS) revert AccountantWithRateProviders__LowerBoundTooLarge();
        uint16 oldBound = accountantState._allowedExchangeRateChangeLower;
        accountantState._allowedExchangeRateChangeLower = _allowedExchangeRateChangeLower;
        emit LowerBoundUpdated(oldBound, _allowedExchangeRateChangeLower);
    }

    /**
     * @notice Update the payout address fees are sent to.
     * @dev Callable by OWNER_ROLE.
     */
    function updatePayoutAddress(address _payoutAddress) external requiresAuth {
        address oldPayout = accountantState._payoutAddress;
        accountantState._payoutAddress = _payoutAddress;
        emit PayoutAddressUpdated(oldPayout, _payoutAddress);
    }

    /**
     * @notice Update the rate provider data for a specific `asset`.
     * @dev Rate providers must return rates in terms of `base`
     * @dev Rate providers MUST ALWAYS return rates in 18 decimals regardless of asset decimals
     * @dev The rate should represent how much base value 1 unit of asset is worth
     * @dev Callable by OWNER_ROLE.
     */
    function setRateProviderData(ERC20 _asset, bool _isPeggedToBase, address _rateProvider) external requiresAuth {
        rateProviderData[_asset] =
            RateProviderData({ isPeggedToBase: _isPeggedToBase, rateProvider: IRateProvider(_rateProvider) });
        emit RateProviderUpdated(address(_asset), _isPeggedToBase, _rateProvider);
    }

    // ========================================= UPDATE EXCHANGE RATE/FEES FUNCTIONS
    // =========================================

    /**
     * @notice Updates this contract exchangeRate.
     * @dev If new exchange rate is outside of accepted bounds, or if not enough time has passed, this
     *      will pause the contract, and this function will NOT calculate fees owed.
     * @dev Callable by UPDATE_EXCHANGE_RATE_ROLE.
     */
    function updateExchangeRate(uint96 _newExchangeRate) external requiresAuth {
        AccountantState storage state = accountantState;

        uint64 currentTime = uint64(block.timestamp);
        (uint96 currentRateWithInterest,) = calculateExchangeRateWithInterest();

        uint96 oldExchangeRate = state._exchangeRate;

        _checkpointInterestAndFees();

        uint256 currentTotalShares = vault.totalSupply();

        if (
            currentTime < state._lastUpdateTimestamp + state._minimumUpdateDelayInSeconds
                || _newExchangeRate
                    > uint256(currentRateWithInterest).mulDivDown(state._allowedExchangeRateChangeUpper, BASIS_POINTS)
                || _newExchangeRate
                    < uint256(currentRateWithInterest).mulDivDown(state._allowedExchangeRateChangeLower, BASIS_POINTS)
        ) {
            pause();
        }

        // Always update the rate and timestamp
        state._exchangeRate = _newExchangeRate;
        state._totalSharesLastUpdate = uint128(currentTotalShares);
        state._lastUpdateTimestamp = currentTime;

        emit ExchangeRateUpdated(oldExchangeRate, _newExchangeRate, currentTime);
    }

    /**
     * @notice Set lending rate
     * @dev Checkpoints current interest and management fees before changing rate
     * @dev This prevents loss of accrued value when rate changes
     * @param _lendingRate New lending rate in basis points (1000 = 10% APY)
     */
    function setLendingRate(uint256 _lendingRate) external requiresAuth {
        require(_lendingRate <= maxLendingRate, "Lending rate exceeds maximum");

        // Checkpoint both interest and fees before rate change
        _checkpointInterestAndFees();

        lendingInfo._lendingRate = _lendingRate;
        emit LendingRateUpdated(_lendingRate, block.timestamp);
    }

    /**
     * @notice Set management fee rate (requires checkpoint)
     * @dev Checkpoints current management fees at old rate before changing
     * @dev This ensures fees are correctly attributed to each rate period
     * @param _managementFeeRate New management fee rate in basis points
     */
    function setManagementFeeRate(uint16 _managementFeeRate) external requiresAuth {
        if (_managementFeeRate > 0.2e4) revert AccountantWithRateProviders__ManagementFeeTooLarge();
        _checkpointInterestAndFees();

        accountantState._managementFee = _managementFeeRate;
        emit ManagementFeeRateUpdated(_managementFeeRate, block.timestamp);
    }

    /**
     * @notice Set maximum lending rate
     * @dev Callable by OWNER_ROLE
     */
    function setMaxLendingRate(uint256 _maxLendingRate) external requiresAuth {
        _checkpointInterestAndFees();
        maxLendingRate = _maxLendingRate;

        // Adjust current rate if needed
        if (lendingInfo._lendingRate > _maxLendingRate) {
            lendingInfo._lendingRate = _maxLendingRate;
            emit LendingRateUpdated(_maxLendingRate, block.timestamp);
        }

        emit MaxLendingRateUpdated(_maxLendingRate);
    }

    /**
     * @notice Claim pending fees.
     * @dev This function must be called by the BoringVault.
     * @dev This function will lose precision if the exchange rate
     *      decimals is greater than the _feeAsset's decimals.
     */
    function claimFees(ERC20 _feeAsset) external {
        if (msg.sender != address(vault)) revert AccountantWithRateProviders__OnlyCallableByBoringVault();

        AccountantState storage state = accountantState;
        if (state._isPaused) revert AccountantWithRateProviders__Paused();

        _checkpointInterestAndFees();

        if (state._feesOwedInBase == 0) revert AccountantWithRateProviders__ZeroFeesOwed();

        // Determine amount of fees owed in _feeAsset
        uint256 feesOwedInFeeAsset;
        RateProviderData memory data = rateProviderData[_feeAsset];
        if (address(_feeAsset) == address(base)) {
            feesOwedInFeeAsset = state._feesOwedInBase;
        } else {
            uint8 feeAssetDecimals = ERC20(_feeAsset).decimals();
            uint256 feesOwedInBaseUsingFeeAssetDecimals =
                _changeDecimals(state._feesOwedInBase, decimals, feeAssetDecimals);
            if (data.isPeggedToBase) {
                feesOwedInFeeAsset = feesOwedInBaseUsingFeeAssetDecimals;
            } else {
                uint256 rate = data.rateProvider.getRate();
                feesOwedInFeeAsset = feesOwedInBaseUsingFeeAssetDecimals.mulDivDown(10 ** 18, rate);
            }
        }

        // Zero out fees owed
        state._feesOwedInBase = 0;

        // Transfer fee asset to payout address
        _feeAsset.safeTransferFrom(msg.sender, state._payoutAddress, feesOwedInFeeAsset);

        emit FeesClaimed(address(_feeAsset), feesOwedInFeeAsset);
    }

    // ========================================= RATE FUNCTIONS =========================================

    /**
     * @notice Get this BoringVault's current rate in the base (real-time with interest).
     */
    function getRate() public view returns (uint256 rate) {
        (uint96 currentRate,) = calculateExchangeRateWithInterest();
        return currentRate;
    }

    /**
     * @notice Calculate current exchange rate including accrued interest
     * @dev This is a view function - interest continues accruing even when paused
     * @return newRate The exchange rate including accrued interest
     * @return interestAccrued The amount of interest accrued since last checkpoint
     */
    function calculateExchangeRateWithInterest() public view returns (uint96 newRate, uint256 interestAccrued) {
        newRate = accountantState._exchangeRate;

        if (lendingInfo._lendingRate > 0) {
            uint256 timeElapsed = block.timestamp - lendingInfo._lastAccrualTime;

            // Calculate rate increase in 18 decimals
            uint256 rateIncrease = uint256(accountantState._exchangeRate).mulDivDown(
                lendingInfo._lendingRate * timeElapsed, SECONDS_PER_YEAR * BASIS_POINTS
            );
            newRate = accountantState._exchangeRate + uint96(rateIncrease);

            // Interest accrued is only for actual deposits
            if (vault.totalSupply() > 0) {
                // Calculate in 18 decimals, then convert to base decimals
                uint256 totalDepositsIn18 = vault.totalSupply().mulDivDown(newRate, ONE_SHARE);
                uint256 totalDeposits = _changeDecimals(totalDepositsIn18, 18, decimals);

                interestAccrued =
                    totalDeposits.mulDivDown(lendingInfo._lendingRate * timeElapsed, SECONDS_PER_YEAR * BASIS_POINTS);
            }
        }
    }

    /**
     * @notice Get this BoringVault's current rate in the base.
     * @dev Revert if paused.
     */
    function getRateSafe() external view returns (uint256 rate) {
        if (accountantState._isPaused) revert AccountantWithRateProviders__Paused();
        (uint96 currentRate,) = calculateExchangeRateWithInterest();
        rate = currentRate;
    }

    /**
     * @notice Get this BoringVault's current rate in the provided quote.
     * @dev `quote` must have its RateProviderData set, else this will revert.
     * @dev This function will lose precision if the exchange rate
     *      decimals is greater than the _quote's decimals.
     */
    function getRateInQuote(ERC20 _quote) public view returns (uint256 rateInQuote) {
        // Get real-time rate in 18 decimals
        (uint96 currentRate,) = calculateExchangeRateWithInterest();

        if (address(_quote) == address(base)) {
            // Convert from 18 decimals to base decimals for display
            rateInQuote = _changeDecimals(currentRate, 18, decimals);
        } else {
            RateProviderData memory data = rateProviderData[_quote];
            uint8 quoteDecimals = ERC20(_quote).decimals();

            if (data.isPeggedToBase) {
                // Convert from 18 decimals to quote decimals
                rateInQuote = _changeDecimals(currentRate, 18, quoteDecimals);
            } else {
                // Rate provider should return 18 decimals
                uint256 quoteRate = data.rateProvider.getRate();
                // Calculate: currentRate * 1e18 / quoteRate, then scale to quote decimals
                uint256 rateInQuote18 = uint256(currentRate).mulDivDown(1e18, quoteRate);
                rateInQuote = _changeDecimals(rateInQuote18, 18, quoteDecimals);
            }
        }
    }

    /**
     * @notice Get this BoringVault's current rate in the provided quote.
     * @dev `quote` must have its RateProviderData set, else this will revert.
     * @dev Revert if paused.
     */
    function getRateInQuoteSafe(ERC20 _quote) external view returns (uint256 rateInQuote) {
        if (accountantState._isPaused) revert AccountantWithRateProviders__Paused();
        rateInQuote = getRateInQuote(_quote);
    }

    /**
     * @notice Get total rate paid by borrower
     * @dev This is the sum of lending rate (for depositors) and management fee rate
     * @return Total borrower rate in basis points
     */
    function getBorrowerRate() public view returns (uint256) {
        return lendingInfo._lendingRate + accountantState._managementFee;
    }

    /**
     * @notice Preview total management fees owed including unclaimed
     * @dev Calculates real-time fees without modifying state
     * @dev Includes both stored fees and fees accrued since last checkpoint
     * @return totalFees Total management fees owed in base asset
     */
    function previewFeesOwed() external view returns (uint256 totalFees) {
        totalFees = accountantState._feesOwedInBase;

        if (vault.totalSupply() > 0 && accountantState._managementFee > 0) {
            uint256 timeElapsed = block.timestamp - lendingInfo._lastAccrualTime;

            (uint96 currentRate,) = calculateExchangeRateWithInterest();
            uint256 totalDepositsIn18 = vault.totalSupply().mulDivDown(currentRate, ONE_SHARE);
            // Convert to base decimals for fee calculation
            uint256 totalDeposits = _changeDecimals(totalDepositsIn18, 18, decimals);

            uint256 managementFees =
                totalDeposits.mulDivDown(accountantState._managementFee * timeElapsed, SECONDS_PER_YEAR * BASIS_POINTS);
            totalFees += managementFees;
        }
    }

    // ========================================= INTERNAL HELPER FUNCTIONS =========================================
    /**
     * @notice Checkpoint both interest and management fees
     * @dev Updates exchange rate with interest and feesOwedInBase with management fees
     */
    function _checkpointInterestAndFees() internal {
        uint256 timeElapsed = block.timestamp - lendingInfo._lastAccrualTime;
        if (timeElapsed > 0) {
            (uint96 newRate,) = calculateExchangeRateWithInterest();
            accountantState._exchangeRate = newRate;

            // Only calculate management fees when there are actual deposits
            if (vault.totalSupply() > 0 && accountantState._managementFee > 0) {
                // Calculate value in 18 decimals, then convert to base decimals for fee storage
                uint256 totalValueIn18 = vault.totalSupply().mulDivDown(newRate, ONE_SHARE);
                uint256 totalValue = _changeDecimals(totalValueIn18, 18, decimals);

                uint256 managementFees =
                    totalValue.mulDivDown(accountantState._managementFee * timeElapsed, SECONDS_PER_YEAR * BASIS_POINTS);
                accountantState._feesOwedInBase += uint128(managementFees);
            }
            lendingInfo._lastAccrualTime = block.timestamp;
            emit Checkpoint(block.timestamp);
        }
    }

    /**
     * @notice Updates the stored exchange rate and accrues management fees
     * @dev Should be called before any operation that depends on the current exchange rate
     * @dev This includes deposits, withdrawals, and fee calculations
     * @dev Callable by authorized contracts (Teller) to ensure rate consistency
     */
    function checkpoint() external requiresAuth {
        require(!accountantState._isPaused, "Cannot checkpoint when paused");
        _checkpointInterestAndFees();
    }

    /**
     * @notice Used to change the decimals of precision used for an amount.
     */
    function _changeDecimals(uint256 _amount, uint8 _fromDecimals, uint8 _toDecimals) internal pure returns (uint256) {
        if (_fromDecimals == _toDecimals) {
            return _amount;
        } else if (_fromDecimals < _toDecimals) {
            return _amount * 10 ** (_toDecimals - _fromDecimals);
        } else {
            return _amount / 10 ** (_fromDecimals - _toDecimals);
        }
    }
}

// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.22;

import { FixedPointMathLib } from "@solmate/utils/FixedPointMathLib.sol";
import { SafeTransferLib } from "@solmate/utils/SafeTransferLib.sol";
import { ERC20 } from "@solmate/tokens/ERC20.sol";
import { ReentrancyGuard } from "@solmate/utils/ReentrancyGuard.sol";
import { IAtomicSolver } from "./IAtomicSolver.sol";
import { AccountantWithRateProviders } from "../base/Roles/AccountantWithRateProviders.sol";

/**
 * @title AtomicQueue
 * @notice Allows users to create `AtomicRequests` that specify an ERC20 asset to `offer`
 *         and an ERC20 asset to `want` in return.
 * @notice Making atomic requests where the exchange rate between offer and want is not
 *         relatively stable is effectively the same as placing a limit order between
 *         those assets, so requests can be filled at a rate worse than the current market rate.
 * @notice It is possible for a user to make multiple requests that use the same offer asset.
 *         If this is done it is important that the user has approved the queue to spend the
 *         total amount of assets aggregated from all their requests, and to also have enough
 *         `offer` asset to cover the aggregate total request of `offerAmount`.
 * @author crispymangoes
 */
contract AtomicQueue is ReentrancyGuard {
    using SafeTransferLib for ERC20;
    using FixedPointMathLib for uint256;

    // ========================================= STRUCTS =========================================

    /**
     * @notice Stores request information needed to fulfill a users atomic request.
     * @param deadline unix timestamp for when request is no longer valid
     * @param offerAmount the amount of `offer` asset the user wants converted to `want` asset
     * @param inSolve bool used during solves to prevent duplicate users, and to prevent redoing multiple checks
     */
    struct AtomicRequest {
        uint64 deadline; // deadline to fulfill request
        uint96 offerAmount; // The amount of offer asset the user wants to sell.
        bool inSolve; // Indicates whether this user is currently having their request fulfilled.
    }

    /**
     * @notice Used in `viewSolveMetaData` helper function to return data in a clean struct.
     * @param user the address of the user
     * @param flags 8 bits indicating the state of the user only the first 4 bits are used XXXX0000
     *              Either all flags are false(user is solvable) or only 1 is true(an error occurred).
     *              From right to left
     *              - 0: indicates user deadline has passed.
     *              - 1: indicates user request has zero offer amount.
     *              - 2: indicates user does not have enough offer asset in wallet.
     *              - 3: indicates user has not given AtomicQueue approval.
     * @param assetsToOffer the amount of offer asset to solve
     * @param assetsForWant the amount of assets users want for their offer assets
     */
    struct SolveMetaData {
        address user;
        uint8 flags;
        uint256 assetsToOffer;
        uint256 assetsForWant;
    }

    // ========================================= GLOBAL STATE =========================================
    AccountantWithRateProviders public immutable accountant;

    /**
     * @notice Maps user address to offer asset to want asset to a AtomicRequest struct.
     */
    mapping(address => mapping(ERC20 => mapping(ERC20 => AtomicRequest))) public userAtomicRequest;

    //============================== ERRORS ===============================

    error AtomicQueue__UserRepeated(address user);
    error AtomicQueue__RequestDeadlineExceeded(address user);
    error AtomicQueue__UserNotInSolve(address user);
    error AtomicQueue__ZeroOfferAmount(address user);
    error AtomicQueue__OnlyCallableInternally();

    //============================== EVENTS ===============================

    /**
     * @notice Emitted when `updateAtomicRequest` is called.
     */
    event AtomicRequestUpdated(
        address user,
        address offerToken,
        address wantToken,
        uint256 amount,
        uint256 deadline,
        uint256 minPrice,
        uint256 timestamp
    );

    /**
     * @notice Emitted when `solve` exchanges a users offer asset for their want asset.
     */
    event AtomicRequestFulfilled(
        address user,
        address offerToken,
        address wantToken,
        uint256 offerAmountSpent,
        uint256 wantAmountReceived,
        uint256 timestamp
    );

    //============================== CONSTRUCTOR ===============================

    constructor(address _accountant) {
        accountant = AccountantWithRateProviders(_accountant);
    }

    //============================== USER FUNCTIONS ===============================

    /**
     * @notice Get a users Atomic Request.
     * @param user the address of the user to get the request for
     * @param offer the ERC0 token they want to exchange for the want
     * @param want the ERC20 token they want in exchange for the offer
     */
    function getUserAtomicRequest(address user, ERC20 offer, ERC20 want) external view returns (AtomicRequest memory) {
        return userAtomicRequest[user][offer][want];
    }

    /**
     * @notice Helper function that returns either
     *         true: Withdraw request is valid.
     *         false: Withdraw request is not valid.
     * @dev It is possible for a withdraw request to return false from this function, but using the
     *      request in `updateAtomicRequest` will succeed, but solvers will not be able to include
     *      the user in `solve` unless some other state is changed.
     * @param offer the ERC0 token they want to exchange for the want
     * @param user the address of the user making the request
     * @param userRequest the request struct to validate
     */
    function isAtomicRequestValid(
        ERC20 offer,
        address user,
        AtomicRequest calldata userRequest
    )
        external
        view
        returns (bool)
    {
        if (userRequest.offerAmount > offer.balanceOf(user)) return false;
        if (block.timestamp > userRequest.deadline) return false;
        if (offer.allowance(user, address(this)) < userRequest.offerAmount) return false;
        if (userRequest.offerAmount == 0) return false;

        return true;
    }

    /**
     * @notice Allows user to add/update their withdraw request.
     * @param offer the ERC20 token the user is offering in exchange for the want
     * @param want the ERC20 token the user wants in exchange for offer
     * @param deadline unix timestamp for when request is no longer valid
     * @param offerAmount the amount of offer asset to exchange
     */
    function updateAtomicRequest(ERC20 offer, ERC20 want, uint64 deadline, uint96 offerAmount) external nonReentrant {
        AtomicRequest storage request = userAtomicRequest[msg.sender][offer][want];

        request.deadline = deadline;
        request.offerAmount = offerAmount;

        emit AtomicRequestUpdated(msg.sender, address(offer), address(want), offerAmount, deadline, 0, block.timestamp);
    }

    //============================== SOLVER FUNCTIONS ===============================

    /**
     * @notice Called by solvers in order to exchange offer asset for want asset.
     * @notice Solvers are optimistically transferred the offer asset, then are required to
     *         approve this contract to spend enough of want assets to cover all requests.
     * @dev It is very likely `solve` TXs will be front run if broadcasted to public mem pools,
     *      so solvers should use private mem pools.
     * @param offer the ERC20 offer token to solve for
     * @param want the ERC20 want token to solve for
     * @param users an array of user addresses to solve for
     * @param runData extra data that is passed back to solver when `finishSolve` is called
     * @param solver the address to make `finishSolve` callback to
     */
    function solve(
        ERC20 offer,
        ERC20 want,
        address[] calldata users,
        bytes calldata runData,
        address solver
    )
        external
        nonReentrant
    {
        (uint256 assetsToOffer, uint256 assetsForWant) = _prepareSolve(offer, want, users, solver);

        IAtomicSolver(solver).finishSolve(runData, msg.sender, offer, want, assetsToOffer, assetsForWant);

        _finalizeSolve(offer, want, users, solver);
    }

    //============================== INTERNAL HELPER FUNCTIONS ===============================
    /**
     * @notice New internal function to handle first phase of solve
     * @dev Validates all user requests and transfers offer tokens to solver
     * @dev Calculates total assets needed using current NAV from accountant
     * @param offer the ERC20 offer token
     * @param want the ERC20 want token
     * @param users array of users to process
     * @param solver the solver address receiving offer tokens
     * @return assetsToOffer total offer tokens transferred to solver
     * @return assetsForWant total want tokens solver needs to provide
     */
    function _prepareSolve(
        ERC20 offer,
        ERC20 want,
        address[] calldata users,
        address solver
    )
        internal
        returns (uint256 assetsToOffer, uint256 assetsForWant)
    {
        uint256 rate = accountant.getRateInQuoteSafe(want);
        uint256 offerDecimals = 10 ** offer.decimals();

        for (uint256 i; i < users.length; ++i) {
            AtomicRequest storage request = userAtomicRequest[users[i]][offer][want];

            if (request.inSolve || block.timestamp > request.deadline || request.offerAmount == 0) {
                continue;
            }

            try this.attemptTransfer(offer, users[i], solver, request.offerAmount) {
                assetsForWant += rate.mulDivDown(request.offerAmount, offerDecimals);
                assetsToOffer += request.offerAmount;
                request.inSolve = true;
            } catch {
                continue;
            }
        }
    }

    /**
     * @notice Internal function to handle final phase of solve
     * @dev Transfers want tokens from solver to users and cleans up state
     * @dev Uses same NAV rate for all calculations to ensure consistency
     * @param offer the ERC20 offer token
     * @param want the ERC20 want token
     * @param users array of users to process
     * @param solver the solver address providing want tokens
     */
    function _finalizeSolve(ERC20 offer, ERC20 want, address[] calldata users, address solver) internal {
        uint256 rate = accountant.getRateInQuoteSafe(want);
        uint256 offerDecimals = 10 ** offer.decimals();

        for (uint256 i; i < users.length; ++i) {
            AtomicRequest storage request = userAtomicRequest[users[i]][offer][want];

            if (request.inSolve) {
                uint256 amountOut = rate.mulDivDown(request.offerAmount, offerDecimals);

                want.safeTransferFrom(solver, users[i], amountOut);

                emit AtomicRequestFulfilled(
                    users[i], address(offer), address(want), request.offerAmount, amountOut, block.timestamp
                );

                request.offerAmount = 0;
                request.deadline = 0;
                request.inSolve = false;
            } else {
                revert AtomicQueue__UserNotInSolve(users[i]);
            }
        }
    }

    /**
     * @notice Helper function solvers can use to determine if users are solvable, and the required amounts to do so.
     * @notice Repeated users are not accounted for in this setup, so if solvers have repeat users in their `users`
     *         array the results can be wrong.
     * @dev Since a user can have multiple requests with the same offer asset but different want asset, it is
     *      possible for `viewSolveMetaData` to report no errors, but for a solve to fail, if any solves were done
     *      between the time `viewSolveMetaData` and before `solve` is called.
     * @param offer the ERC20 offer token to check for solvability
     * @param want the ERC20 want token to check for solvability
     * @param users an array of user addresses to check for solvability
     */
    function viewSolveMetaData(
        ERC20 offer,
        ERC20 want,
        address[] calldata users
    )
        external
        view
        returns (SolveMetaData[] memory metaData, uint256 totalAssetsForWant, uint256 totalAssetsToOffer)
    {
        metaData = new SolveMetaData[](users.length);
        uint256 rate = accountant.getRateInQuoteSafe(want);
        uint256 offerDecimalsPow = 10 ** offer.decimals();

        for (uint256 i; i < users.length; ++i) {
            AtomicRequest memory request = userAtomicRequest[users[i]][offer][want];
            metaData[i].user = users[i];

            if (block.timestamp > request.deadline) {
                metaData[i].flags |= uint8(1);
            }
            if (request.offerAmount == 0) {
                metaData[i].flags |= uint8(1) << 1;
            }
            if (offer.balanceOf(users[i]) < request.offerAmount) {
                metaData[i].flags |= uint8(1) << 2;
            }
            if (offer.allowance(users[i], address(this)) < request.offerAmount) {
                metaData[i].flags |= uint8(1) << 3;
            }

            metaData[i].assetsToOffer = request.offerAmount;
            metaData[i].assetsForWant = rate.mulDivDown(request.offerAmount, offerDecimalsPow);

            if (metaData[i].flags == 0) {
                totalAssetsForWant += metaData[i].assetsForWant;
                totalAssetsToOffer += request.offerAmount;
            }
        }
    }

    /**
     * @notice External helper function to enable try-catch with safeTransferFrom
     * @dev Only callable by this contract itself
     * @param token The ERC20 token to transfer
     * @param from The address to transfer from
     * @param to The address to transfer to
     * @param amount The amount to transfer
     */
    function attemptTransfer(ERC20 token, address from, address to, uint256 amount) external {
        if (msg.sender != address(this)) revert AtomicQueue__OnlyCallableInternally();
        token.safeTransferFrom(from, to, amount);
    }
}


Vulnerability type: CWE-682|CWE-1339