DSPay

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/DSPay.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;

import {
    AccessControlDefaultAdminRules
} from "@openzeppelin/contracts/access/extensions/AccessControlDefaultAdminRules.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

import {IDSPay} from "./interfaces/IDSPay.sol";
import {AssetManagement} from "./libraries/AssetManagement.sol";
import {MerchantLogic} from "./libraries/MerchantLogic.sol";
import {SwapLogic} from "./libraries/SwapLogic.sol";
import {PayWallLogic} from "./libraries/PayWallLogic.sol";
import {SafeExecutor} from "./SafeExecutor.sol";
import {PendingPayment} from "./libraries/PendingPayment.sol";
import {PaymentLogic} from "./module/PaymentLogic.sol";
import {ZERO_ADDRESS} from "./libraries/Constants.sol";

// slither-disable-next-line locked-ether
contract DSPay is IDSPay, AccessControlDefaultAdminRules, ReentrancyGuard {
    using AssetManagement for mapping(address asset => AssetManagement.PaymentAsset);
    using MerchantLogic for MerchantLogic.MerchantLogicStorage;
    using SwapLogic for SwapLogic.SwapLogicStorage;
    using PayWallLogic for PayWallLogic.PayWallLogicStorage;
    using PendingPayment for PendingPayment.PendingPaymentStorage;

    error InvalidMerchant();
    /// @notice Error thrown when the merchant address is zero
    error MerchantAddressCannotBeZero();

    /// @notice Enum representing the payment method that triggered a callback
    enum PaymentMethod {
        SEND,
        AUTHORIZE
    }

    struct PaymentMetadata {
        address payoutToken;
        uint256 payoutAmount;
        uint248 amountInUSD;
        bytes32 onBehalfOf;
        address sender;
        bytes32 itemId;
        address merchant;
        PaymentMethod method;
    }

    struct SendWithCallbackParams {
        address asset;
        uint248 amount;
        bytes32 onBehalfOf;
        address merchant;
        bytes32 itemId;
        bytes callbackData;
        bytes customSourceAssetPath;
    }

    // solhint-disable-next-line gas-struct-packing
    struct DSPayStorage {
        address executorAddress;
        mapping(address asset => AssetManagement.PaymentAsset) assets;
        AssetManagement.AssetManagementSequencerConfig assetManagementSequencerConfig;
        MerchantLogic.MerchantLogicStorage merchantLogicStorage;
        SwapLogic.SwapLogicStorage swapLogicStorage;
        PayWallLogic.PayWallLogicStorage paywallLogicStorage;
        PendingPayment.PendingPaymentStorage pendingPaymentStorage;
    }

    DSPayStorage internal _dsPayStorage;

    constructor(
        address admin,
        SwapLogic.SwapLogicConfig memory swapLogicConfig,
        AssetManagement.AssetManagementSequencerConfig memory sequencerConfig
    ) AccessControlDefaultAdminRules(0, admin) {
        _deployExecutor();
        _dsPayStorage.swapLogicStorage.setConfig(swapLogicConfig);
        _dsPayStorage.assetManagementSequencerConfig = sequencerConfig;
    }

    function _deployExecutor() internal {
        _dsPayStorage.executorAddress = address(new SafeExecutor());
    }

    /// @inheritdoc IDSPay
    function getExecutorAddress() external view returns (address executor) {
        return _dsPayStorage.executorAddress;
    }

    /// @inheritdoc IDSPay
    function setPaymentAsset(
        address assetAddress,
        AssetManagement.PaymentAsset calldata paymentAsset,
        bytes calldata path
    ) external onlyRole(DEFAULT_ADMIN_ROLE) {
        address originAsset = SwapLogic.calldataExtractPathOriginAsset(path);
        if (originAsset != assetAddress) {
            revert SwapLogic.InvalidPath();
        }

        AssetManagement.set(_dsPayStorage.assets, assetAddress, paymentAsset);
        _dsPayStorage.swapLogicStorage.setSourceAssetPath(path);
    }

    /// @inheritdoc IDSPay
    function removePaymentAsset(address asset) external onlyRole(DEFAULT_ADMIN_ROLE) {
        AssetManagement.remove(_dsPayStorage.assets, asset);
    }

    /// @inheritdoc IDSPay
    function getPaymentAsset(address asset) external view returns (AssetManagement.PaymentAsset memory paymentAsset) {
        return AssetManagement.get(_dsPayStorage.assets, asset);
    }

    /// @inheritdoc IDSPay
    function getSequencerConfig()
        external
        view
        returns (AssetManagement.AssetManagementSequencerConfig memory sequencerConfig)
    {
        return _dsPayStorage.assetManagementSequencerConfig;
    }

    function _sendPayment(
        address asset,
        uint248 amount,
        bytes32 onBehalfOf,
        address merchant,
        bytes calldata memo,
        bytes32 itemId,
        bytes memory customSourceAssetPath
    ) internal returns (PaymentLogic.ProcessPaymentResult memory result) {
        if (merchant == ZERO_ADDRESS) {
            revert MerchantAddressCannotBeZero();
        }
        result = PaymentLogic.processPayment(
            _dsPayStorage,
            PaymentLogic.ProcessPaymentParams({
                asset: asset,
                amount: amount,
                merchant: merchant,
                itemId: itemId,
                customSourceAssetPath: customSourceAssetPath
            })
        );

        emit SendPayment(asset, amount, onBehalfOf, merchant, memo, result.amountInUSD, msg.sender, itemId);
    }

    /// @inheritdoc IDSPay
    function send(
        address asset,
        uint248 amount,
        bytes32 onBehalfOf,
        address merchant,
        bytes calldata memo,
        bytes32 itemId
    ) external nonReentrant {
        _sendPayment(asset, amount, onBehalfOf, merchant, memo, itemId, "");
    }

    /// @inheritdoc IDSPay
    function sendPathOverride(
        bytes calldata customSourceAssetPath,
        uint248 amount,
        bytes32 onBehalfOf,
        address merchant,
        bytes calldata memo,
        bytes32 itemId
    ) external nonReentrant {
        if (merchant == ZERO_ADDRESS) {
            revert MerchantAddressCannotBeZero();
        }
        if (!_dsPayStorage.merchantLogicStorage.getConfig(merchant).allowUserCustomPath) {
            revert MerchantLogic.UserCustomPathNotAllowed();
        }
        _sendPayment(
            SwapLogic.calldataExtractPathOriginAsset(customSourceAssetPath),
            amount,
            onBehalfOf,
            merchant,
            memo,
            itemId,
            customSourceAssetPath
        );
    }

    function _sendWithCallback(SendWithCallbackParams memory params, bytes calldata memo) internal {
        MerchantLogic.ItemIdCallbackConfig memory callbackConfig =
            _dsPayStorage.merchantLogicStorage.getItemIdCallback(params.merchant, params.itemId);

        if (callbackConfig.acceptedMethods & MerchantLogic.SEND_METHOD == 0) {
            revert MerchantLogic.UnsupportedPaymentMethod();
        }

        PaymentLogic.ProcessPaymentResult memory result = _sendPayment(
            params.asset,
            params.amount,
            params.onBehalfOf,
            params.merchant,
            memo,
            params.itemId,
            params.customSourceAssetPath
        );

        bytes memory fullCallbackData;
        if (callbackConfig.includePaymentMetadata) {
            PaymentMetadata memory metadata = PaymentMetadata({
                payoutToken: result.payoutToken,
                payoutAmount: result.receivedPayoutAmount,
                amountInUSD: result.amountInUSD,
                onBehalfOf: params.onBehalfOf,
                sender: msg.sender,
                itemId: params.itemId,
                merchant: params.merchant,
                method: PaymentMethod.SEND
            });

            // slither-disable-next-line encode-packed-collision
            fullCallbackData = abi.encodePacked(callbackConfig.funcSig, abi.encode(metadata), params.callbackData);
        } else {
            fullCallbackData = abi.encodePacked(callbackConfig.funcSig, params.callbackData);
        }

        SafeExecutor(_dsPayStorage.executorAddress).execute(callbackConfig.contractAddress, fullCallbackData);
    }

    /// @inheritdoc IDSPay
    function sendWithCallbackPathOverride(
        bytes calldata customSourceAssetPath,
        uint248 amount,
        bytes32 onBehalfOf,
        address merchant,
        bytes calldata memo,
        bytes32 itemId,
        bytes calldata callbackData
    ) external nonReentrant {
        if (merchant == ZERO_ADDRESS) {
            revert MerchantAddressCannotBeZero();
        }
        if (!_dsPayStorage.merchantLogicStorage.getConfig(merchant).allowUserCustomPath) {
            revert MerchantLogic.UserCustomPathNotAllowed();
        }
        _sendWithCallback(
            SendWithCallbackParams({
                asset: SwapLogic.calldataExtractPathOriginAsset(customSourceAssetPath),
                amount: amount,
                onBehalfOf: onBehalfOf,
                merchant: merchant,
                itemId: itemId,
                callbackData: callbackData,
                customSourceAssetPath: customSourceAssetPath
            }),
            memo
        );
    }

    /// @inheritdoc IDSPay
    function sendWithCallback(
        address asset,
        uint248 amount,
        bytes32 onBehalfOf,
        address merchant,
        bytes calldata memo,
        bytes32 itemId,
        bytes calldata callbackData
    ) external nonReentrant {
        _sendWithCallback(
            SendWithCallbackParams({
                asset: asset,
                amount: amount,
                onBehalfOf: onBehalfOf,
                merchant: merchant,
                itemId: itemId,
                callbackData: callbackData,
                customSourceAssetPath: ""
            }),
            memo
        );
    }

    function _authorize(
        address asset,
        uint248 amount,
        bytes32 onBehalfOf,
        address merchant,
        bytes calldata memo,
        bytes32 itemId
    ) internal returns (PaymentMetadata memory metadata) {
        if (merchant == ZERO_ADDRESS) {
            revert MerchantAddressCannotBeZero();
        }
        PaymentLogic.AuthorizePaymentParams memory params =
            PaymentLogic.AuthorizePaymentParams({asset: asset, amount: amount, merchant: merchant, itemId: itemId});

        (PendingPayment.Transaction memory transaction, bytes32 transactionHash, uint248 amountInUSD) =
            PaymentLogic.authorizePayment(_dsPayStorage, params);

        emit Authorized(transaction, transactionHash, onBehalfOf, memo, itemId);

        address payoutToken = _dsPayStorage.swapLogicStorage.getMerchantPayoutAsset(merchant);

        metadata = PaymentMetadata({
            payoutToken: payoutToken,
            payoutAmount: 0,
            amountInUSD: amountInUSD,
            onBehalfOf: onBehalfOf,
            sender: msg.sender,
            itemId: itemId,
            merchant: merchant,
            method: PaymentMethod.AUTHORIZE
        });
    }

    /// @inheritdoc IDSPay
    function authorize(
        address asset,
        uint248 amount,
        bytes32 onBehalfOf,
        address merchant,
        bytes calldata memo,
        bytes32 itemId
    ) external nonReentrant {
        _authorize(asset, amount, onBehalfOf, merchant, memo, itemId);
    }

    /// @inheritdoc IDSPay
    function authorizeWithCallback(
        address asset,
        uint248 amount,
        bytes32 onBehalfOf,
        address merchant,
        bytes calldata memo,
        bytes32 itemId,
        bytes calldata callbackData
    ) external nonReentrant {
        MerchantLogic.ItemIdCallbackConfig memory callbackConfig =
            _dsPayStorage.merchantLogicStorage.getItemIdCallback(merchant, itemId);

        if (callbackConfig.acceptedMethods & MerchantLogic.AUTHORIZE_METHOD == 0) {
            revert MerchantLogic.UnsupportedPaymentMethod();
        }

        PaymentMetadata memory metadata = _authorize(asset, amount, onBehalfOf, merchant, memo, itemId);

        bytes memory fullCallbackData;
        if (callbackConfig.includePaymentMetadata) {
            // slither-disable-next-line encode-packed-collision
            fullCallbackData = abi.encodePacked(callbackConfig.funcSig, abi.encode(metadata), callbackData);
        } else {
            fullCallbackData = abi.encodePacked(callbackConfig.funcSig, callbackData);
        }

        SafeExecutor(_dsPayStorage.executorAddress).execute(callbackConfig.contractAddress, fullCallbackData);
    }

    /// @inheritdoc IDSPay
    function setMerchantConfig(MerchantLogic.MerchantConfig calldata config, bytes calldata path) external {
        _dsPayStorage.merchantLogicStorage.setConfig(msg.sender, config);
        _dsPayStorage.swapLogicStorage.setMerchantTargetAssetPath(msg.sender, path);
    }

    /// @inheritdoc IDSPay
    function getMerchantConfig(address merchant) external view returns (MerchantLogic.MerchantConfig memory config) {
        return _dsPayStorage.merchantLogicStorage.getConfig(merchant);
    }

    /// @inheritdoc IDSPay
    function setPaywallItemPrice(bytes32 item, uint248 price) external {
        _dsPayStorage.paywallLogicStorage.setItemPrice(msg.sender, item, price);
    }

    /// @inheritdoc IDSPay
    function getPaywallItemPrice(bytes32 item, address merchant) external view returns (uint248 price) {
        return _dsPayStorage.paywallLogicStorage.getItemPrice(merchant, item);
    }

    function _settleAuthorizedPayment(
        bytes memory customSourceAssetPath,
        address sourceAsset,
        uint248 sourceAssetAmount,
        address from,
        address merchant,
        bytes32 transactionHash,
        uint248 maxUsdValueOfTargetToken
    ) internal {
        PaymentLogic.ProcessSettlementResult memory result = PaymentLogic.processSettlement(
            _dsPayStorage,
            PaymentLogic.ProcessSettlementParams({
                customSourceAssetPath: customSourceAssetPath,
                sourceAsset: sourceAsset,
                sourceAssetAmount: sourceAssetAmount,
                from: from,
                merchant: merchant,
                transactionHash: transactionHash,
                maxUsdValueOfTargetToken: maxUsdValueOfTargetToken
            })
        );

        emit AuthorizedPaymentSettled(
            sourceAsset,
            sourceAssetAmount,
            result.payoutToken,
            result.receivedTargetAssetAmount,
            result.receivedRefundAmount,
            from,
            merchant,
            transactionHash
        );
    }

    /// @inheritdoc IDSPay
    function settleAuthorizedPayment(
        address sourceAsset,
        uint248 sourceAssetAmount,
        address from,
        bytes32 transactionHash,
        uint248 maxUsdValueOfTargetToken
    ) external nonReentrant {
        _settleAuthorizedPayment(
            "", sourceAsset, sourceAssetAmount, from, msg.sender, transactionHash, maxUsdValueOfTargetToken
        );
    }

    /// @inheritdoc IDSPay
    function settleAuthorizedPaymentPathOverride(
        bytes calldata customSourceAssetPath,
        uint248 sourceAssetAmount,
        address from,
        bytes32 transactionHash,
        uint248 maxUsdValueOfTargetToken
    ) external nonReentrant {
        address sourceAsset = SwapLogic.calldataExtractPathOriginAsset(customSourceAssetPath);

        _settleAuthorizedPayment(
            customSourceAssetPath,
            sourceAsset,
            sourceAssetAmount,
            from,
            msg.sender,
            transactionHash,
            maxUsdValueOfTargetToken
        );
    }

    function setItemIdCallbackConfig(bytes32 itemId, MerchantLogic.ItemIdCallbackConfig calldata config) external {
        _dsPayStorage.merchantLogicStorage.setItemIdCallback(msg.sender, itemId, config);
    }

    function getItemIdCallbackConfig(address merchant, bytes32 itemId)
        external
        view
        returns (MerchantLogic.ItemIdCallbackConfig memory config)
    {
        return _dsPayStorage.merchantLogicStorage.getItemIdCallback(merchant, itemId);
    }
}
"
    },
    "dependencies/@openzeppelin-contracts-5.2.0/access/extensions/AccessControlDefaultAdminRules.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (access/extensions/AccessControlDefaultAdminRules.sol)

pragma solidity ^0.8.20;

import {IAccessControlDefaultAdminRules} from "./IAccessControlDefaultAdminRules.sol";
import {AccessControl, IAccessControl} from "../AccessControl.sol";
import {SafeCast} from "../../utils/math/SafeCast.sol";
import {Math} from "../../utils/math/Math.sol";
import {IERC5313} from "../../interfaces/IERC5313.sol";

/**
 * @dev Extension of {AccessControl} that allows specifying special rules to manage
 * the `DEFAULT_ADMIN_ROLE` holder, which is a sensitive role with special permissions
 * over other roles that may potentially have privileged rights in the system.
 *
 * If a specific role doesn't have an admin role assigned, the holder of the
 * `DEFAULT_ADMIN_ROLE` will have the ability to grant it and revoke it.
 *
 * This contract implements the following risk mitigations on top of {AccessControl}:
 *
 * * Only one account holds the `DEFAULT_ADMIN_ROLE` since deployment until it's potentially renounced.
 * * Enforces a 2-step process to transfer the `DEFAULT_ADMIN_ROLE` to another account.
 * * Enforces a configurable delay between the two steps, with the ability to cancel before the transfer is accepted.
 * * The delay can be changed by scheduling, see {changeDefaultAdminDelay}.
 * * It is not possible to use another role to manage the `DEFAULT_ADMIN_ROLE`.
 *
 * Example usage:
 *
 * ```solidity
 * contract MyToken is AccessControlDefaultAdminRules {
 *   constructor() AccessControlDefaultAdminRules(
 *     3 days,
 *     msg.sender // Explicit initial `DEFAULT_ADMIN_ROLE` holder
 *    ) {}
 * }
 * ```
 */
abstract contract AccessControlDefaultAdminRules is IAccessControlDefaultAdminRules, IERC5313, AccessControl {
    // pending admin pair read/written together frequently
    address private _pendingDefaultAdmin;
    uint48 private _pendingDefaultAdminSchedule; // 0 == unset

    uint48 private _currentDelay;
    address private _currentDefaultAdmin;

    // pending delay pair read/written together frequently
    uint48 private _pendingDelay;
    uint48 private _pendingDelaySchedule; // 0 == unset

    /**
     * @dev Sets the initial values for {defaultAdminDelay} and {defaultAdmin} address.
     */
    constructor(uint48 initialDelay, address initialDefaultAdmin) {
        if (initialDefaultAdmin == address(0)) {
            revert AccessControlInvalidDefaultAdmin(address(0));
        }
        _currentDelay = initialDelay;
        _grantRole(DEFAULT_ADMIN_ROLE, initialDefaultAdmin);
    }

    /**
     * @dev See {IERC165-supportsInterface}.
     */
    function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
        return interfaceId == type(IAccessControlDefaultAdminRules).interfaceId || super.supportsInterface(interfaceId);
    }

    /**
     * @dev See {IERC5313-owner}.
     */
    function owner() public view virtual returns (address) {
        return defaultAdmin();
    }

    ///
    /// Override AccessControl role management
    ///

    /**
     * @dev See {AccessControl-grantRole}. Reverts for `DEFAULT_ADMIN_ROLE`.
     */
    function grantRole(bytes32 role, address account) public virtual override(AccessControl, IAccessControl) {
        if (role == DEFAULT_ADMIN_ROLE) {
            revert AccessControlEnforcedDefaultAdminRules();
        }
        super.grantRole(role, account);
    }

    /**
     * @dev See {AccessControl-revokeRole}. Reverts for `DEFAULT_ADMIN_ROLE`.
     */
    function revokeRole(bytes32 role, address account) public virtual override(AccessControl, IAccessControl) {
        if (role == DEFAULT_ADMIN_ROLE) {
            revert AccessControlEnforcedDefaultAdminRules();
        }
        super.revokeRole(role, account);
    }

    /**
     * @dev See {AccessControl-renounceRole}.
     *
     * For the `DEFAULT_ADMIN_ROLE`, it only allows renouncing in two steps by first calling
     * {beginDefaultAdminTransfer} to the `address(0)`, so it's required that the {pendingDefaultAdmin} schedule
     * has also passed when calling this function.
     *
     * After its execution, it will not be possible to call `onlyRole(DEFAULT_ADMIN_ROLE)` functions.
     *
     * NOTE: Renouncing `DEFAULT_ADMIN_ROLE` will leave the contract without a {defaultAdmin},
     * thereby disabling any functionality that is only available for it, and the possibility of reassigning a
     * non-administrated role.
     */
    function renounceRole(bytes32 role, address account) public virtual override(AccessControl, IAccessControl) {
        if (role == DEFAULT_ADMIN_ROLE && account == defaultAdmin()) {
            (address newDefaultAdmin, uint48 schedule) = pendingDefaultAdmin();
            if (newDefaultAdmin != address(0) || !_isScheduleSet(schedule) || !_hasSchedulePassed(schedule)) {
                revert AccessControlEnforcedDefaultAdminDelay(schedule);
            }
            delete _pendingDefaultAdminSchedule;
        }
        super.renounceRole(role, account);
    }

    /**
     * @dev See {AccessControl-_grantRole}.
     *
     * For `DEFAULT_ADMIN_ROLE`, it only allows granting if there isn't already a {defaultAdmin} or if the
     * role has been previously renounced.
     *
     * NOTE: Exposing this function through another mechanism may make the `DEFAULT_ADMIN_ROLE`
     * assignable again. Make sure to guarantee this is the expected behavior in your implementation.
     */
    function _grantRole(bytes32 role, address account) internal virtual override returns (bool) {
        if (role == DEFAULT_ADMIN_ROLE) {
            if (defaultAdmin() != address(0)) {
                revert AccessControlEnforcedDefaultAdminRules();
            }
            _currentDefaultAdmin = account;
        }
        return super._grantRole(role, account);
    }

    /**
     * @dev See {AccessControl-_revokeRole}.
     */
    function _revokeRole(bytes32 role, address account) internal virtual override returns (bool) {
        if (role == DEFAULT_ADMIN_ROLE && account == defaultAdmin()) {
            delete _currentDefaultAdmin;
        }
        return super._revokeRole(role, account);
    }

    /**
     * @dev See {AccessControl-_setRoleAdmin}. Reverts for `DEFAULT_ADMIN_ROLE`.
     */
    function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual override {
        if (role == DEFAULT_ADMIN_ROLE) {
            revert AccessControlEnforcedDefaultAdminRules();
        }
        super._setRoleAdmin(role, adminRole);
    }

    ///
    /// AccessControlDefaultAdminRules accessors
    ///

    /**
     * @inheritdoc IAccessControlDefaultAdminRules
     */
    function defaultAdmin() public view virtual returns (address) {
        return _currentDefaultAdmin;
    }

    /**
     * @inheritdoc IAccessControlDefaultAdminRules
     */
    function pendingDefaultAdmin() public view virtual returns (address newAdmin, uint48 schedule) {
        return (_pendingDefaultAdmin, _pendingDefaultAdminSchedule);
    }

    /**
     * @inheritdoc IAccessControlDefaultAdminRules
     */
    function defaultAdminDelay() public view virtual returns (uint48) {
        uint48 schedule = _pendingDelaySchedule;
        return (_isScheduleSet(schedule) && _hasSchedulePassed(schedule)) ? _pendingDelay : _currentDelay;
    }

    /**
     * @inheritdoc IAccessControlDefaultAdminRules
     */
    function pendingDefaultAdminDelay() public view virtual returns (uint48 newDelay, uint48 schedule) {
        schedule = _pendingDelaySchedule;
        return (_isScheduleSet(schedule) && !_hasSchedulePassed(schedule)) ? (_pendingDelay, schedule) : (0, 0);
    }

    /**
     * @inheritdoc IAccessControlDefaultAdminRules
     */
    function defaultAdminDelayIncreaseWait() public view virtual returns (uint48) {
        return 5 days;
    }

    ///
    /// AccessControlDefaultAdminRules public and internal setters for defaultAdmin/pendingDefaultAdmin
    ///

    /**
     * @inheritdoc IAccessControlDefaultAdminRules
     */
    function beginDefaultAdminTransfer(address newAdmin) public virtual onlyRole(DEFAULT_ADMIN_ROLE) {
        _beginDefaultAdminTransfer(newAdmin);
    }

    /**
     * @dev See {beginDefaultAdminTransfer}.
     *
     * Internal function without access restriction.
     */
    function _beginDefaultAdminTransfer(address newAdmin) internal virtual {
        uint48 newSchedule = SafeCast.toUint48(block.timestamp) + defaultAdminDelay();
        _setPendingDefaultAdmin(newAdmin, newSchedule);
        emit DefaultAdminTransferScheduled(newAdmin, newSchedule);
    }

    /**
     * @inheritdoc IAccessControlDefaultAdminRules
     */
    function cancelDefaultAdminTransfer() public virtual onlyRole(DEFAULT_ADMIN_ROLE) {
        _cancelDefaultAdminTransfer();
    }

    /**
     * @dev See {cancelDefaultAdminTransfer}.
     *
     * Internal function without access restriction.
     */
    function _cancelDefaultAdminTransfer() internal virtual {
        _setPendingDefaultAdmin(address(0), 0);
    }

    /**
     * @inheritdoc IAccessControlDefaultAdminRules
     */
    function acceptDefaultAdminTransfer() public virtual {
        (address newDefaultAdmin, ) = pendingDefaultAdmin();
        if (_msgSender() != newDefaultAdmin) {
            // Enforce newDefaultAdmin explicit acceptance.
            revert AccessControlInvalidDefaultAdmin(_msgSender());
        }
        _acceptDefaultAdminTransfer();
    }

    /**
     * @dev See {acceptDefaultAdminTransfer}.
     *
     * Internal function without access restriction.
     */
    function _acceptDefaultAdminTransfer() internal virtual {
        (address newAdmin, uint48 schedule) = pendingDefaultAdmin();
        if (!_isScheduleSet(schedule) || !_hasSchedulePassed(schedule)) {
            revert AccessControlEnforcedDefaultAdminDelay(schedule);
        }
        _revokeRole(DEFAULT_ADMIN_ROLE, defaultAdmin());
        _grantRole(DEFAULT_ADMIN_ROLE, newAdmin);
        delete _pendingDefaultAdmin;
        delete _pendingDefaultAdminSchedule;
    }

    ///
    /// AccessControlDefaultAdminRules public and internal setters for defaultAdminDelay/pendingDefaultAdminDelay
    ///

    /**
     * @inheritdoc IAccessControlDefaultAdminRules
     */
    function changeDefaultAdminDelay(uint48 newDelay) public virtual onlyRole(DEFAULT_ADMIN_ROLE) {
        _changeDefaultAdminDelay(newDelay);
    }

    /**
     * @dev See {changeDefaultAdminDelay}.
     *
     * Internal function without access restriction.
     */
    function _changeDefaultAdminDelay(uint48 newDelay) internal virtual {
        uint48 newSchedule = SafeCast.toUint48(block.timestamp) + _delayChangeWait(newDelay);
        _setPendingDelay(newDelay, newSchedule);
        emit DefaultAdminDelayChangeScheduled(newDelay, newSchedule);
    }

    /**
     * @inheritdoc IAccessControlDefaultAdminRules
     */
    function rollbackDefaultAdminDelay() public virtual onlyRole(DEFAULT_ADMIN_ROLE) {
        _rollbackDefaultAdminDelay();
    }

    /**
     * @dev See {rollbackDefaultAdminDelay}.
     *
     * Internal function without access restriction.
     */
    function _rollbackDefaultAdminDelay() internal virtual {
        _setPendingDelay(0, 0);
    }

    /**
     * @dev Returns the amount of seconds to wait after the `newDelay` will
     * become the new {defaultAdminDelay}.
     *
     * The value returned guarantees that if the delay is reduced, it will go into effect
     * after a wait that honors the previously set delay.
     *
     * See {defaultAdminDelayIncreaseWait}.
     */
    function _delayChangeWait(uint48 newDelay) internal view virtual returns (uint48) {
        uint48 currentDelay = defaultAdminDelay();

        // When increasing the delay, we schedule the delay change to occur after a period of "new delay" has passed, up
        // to a maximum given by defaultAdminDelayIncreaseWait, by default 5 days. For example, if increasing from 1 day
        // to 3 days, the new delay will come into effect after 3 days. If increasing from 1 day to 10 days, the new
        // delay will come into effect after 5 days. The 5 day wait period is intended to be able to fix an error like
        // using milliseconds instead of seconds.
        //
        // When decreasing the delay, we wait the difference between "current delay" and "new delay". This guarantees
        // that an admin transfer cannot be made faster than "current delay" at the time the delay change is scheduled.
        // For example, if decreasing from 10 days to 3 days, the new delay will come into effect after 7 days.
        return
            newDelay > currentDelay
                ? uint48(Math.min(newDelay, defaultAdminDelayIncreaseWait())) // no need to safecast, both inputs are uint48
                : currentDelay - newDelay;
    }

    ///
    /// Private setters
    ///

    /**
     * @dev Setter of the tuple for pending admin and its schedule.
     *
     * May emit a DefaultAdminTransferCanceled event.
     */
    function _setPendingDefaultAdmin(address newAdmin, uint48 newSchedule) private {
        (, uint48 oldSchedule) = pendingDefaultAdmin();

        _pendingDefaultAdmin = newAdmin;
        _pendingDefaultAdminSchedule = newSchedule;

        // An `oldSchedule` from `pendingDefaultAdmin()` is only set if it hasn't been accepted.
        if (_isScheduleSet(oldSchedule)) {
            // Emit for implicit cancellations when another default admin was scheduled.
            emit DefaultAdminTransferCanceled();
        }
    }

    /**
     * @dev Setter of the tuple for pending delay and its schedule.
     *
     * May emit a DefaultAdminDelayChangeCanceled event.
     */
    function _setPendingDelay(uint48 newDelay, uint48 newSchedule) private {
        uint48 oldSchedule = _pendingDelaySchedule;

        if (_isScheduleSet(oldSchedule)) {
            if (_hasSchedulePassed(oldSchedule)) {
                // Materialize a virtual delay
                _currentDelay = _pendingDelay;
            } else {
                // Emit for implicit cancellations when another delay was scheduled.
                emit DefaultAdminDelayChangeCanceled();
            }
        }

        _pendingDelay = newDelay;
        _pendingDelaySchedule = newSchedule;
    }

    ///
    /// Private helpers
    ///

    /**
     * @dev Defines if an `schedule` is considered set. For consistency purposes.
     */
    function _isScheduleSet(uint48 schedule) private pure returns (bool) {
        return schedule != 0;
    }

    /**
     * @dev Defines if an `schedule` is considered passed. For consistency purposes.
     */
    function _hasSchedulePassed(uint48 schedule) private view returns (bool) {
        return schedule < block.timestamp;
    }
}
"
    },
    "dependencies/@openzeppelin-contracts-5.2.0/utils/ReentrancyGuard.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (utils/ReentrancyGuard.sol)

pragma solidity ^0.8.20;

/**
 * @dev Contract module that helps prevent reentrant calls to a function.
 *
 * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier
 * available, which can be applied to functions to make sure there are no nested
 * (reentrant) calls to them.
 *
 * Note that because there is a single `nonReentrant` guard, functions marked as
 * `nonReentrant` may not call one another. This can be worked around by making
 * those functions `private`, and then adding `external` `nonReentrant` entry
 * points to them.
 *
 * TIP: If EIP-1153 (transient storage) is available on the chain you're deploying at,
 * consider using {ReentrancyGuardTransient} instead.
 *
 * TIP: If you would like to learn more about reentrancy and alternative ways
 * to protect against it, check out our blog post
 * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].
 */
abstract contract ReentrancyGuard {
    // Booleans are more expensive than uint256 or any type that takes up a full
    // word because each write operation emits an extra SLOAD to first read the
    // slot's contents, replace the bits taken up by the boolean, and then write
    // back. This is the compiler's defense against contract upgrades and
    // pointer aliasing, and it cannot be disabled.

    // The values being non-zero value makes deployment a bit more expensive,
    // but in exchange the refund on every call to nonReentrant will be lower in
    // amount. Since refunds are capped to a percentage of the total
    // transaction's gas, it is best to keep them low in cases like this one, to
    // increase the likelihood of the full refund coming into effect.
    uint256 private constant NOT_ENTERED = 1;
    uint256 private constant ENTERED = 2;

    uint256 private _status;

    /**
     * @dev Unauthorized reentrant call.
     */
    error ReentrancyGuardReentrantCall();

    constructor() {
        _status = NOT_ENTERED;
    }

    /**
     * @dev Prevents a contract from calling itself, directly or indirectly.
     * Calling a `nonReentrant` function from another `nonReentrant`
     * function is not supported. It is possible to prevent this from happening
     * by making the `nonReentrant` function external, and making it call a
     * `private` function that does the actual work.
     */
    modifier nonReentrant() {
        _nonReentrantBefore();
        _;
        _nonReentrantAfter();
    }

    function _nonReentrantBefore() private {
        // On the first call to nonReentrant, _status will be NOT_ENTERED
        if (_status == ENTERED) {
            revert ReentrancyGuardReentrantCall();
        }

        // Any calls to nonReentrant after this point will fail
        _status = ENTERED;
    }

    function _nonReentrantAfter() private {
        // By storing the original value once again, a refund is triggered (see
        // https://eips.ethereum.org/EIPS/eip-2200)
        _status = NOT_ENTERED;
    }

    /**
     * @dev Returns true if the reentrancy guard is currently set to "entered", which indicates there is a
     * `nonReentrant` function in the call stack.
     */
    function _reentrancyGuardEntered() internal view returns (bool) {
        return _status == ENTERED;
    }
}
"
    },
    "src/interfaces/IDSPay.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;

import {AssetManagement} from "../libraries/AssetManagement.sol";
import {MerchantLogic} from "../libraries/MerchantLogic.sol";
import {PendingPayment} from "../libraries/PendingPayment.sol";

interface IDSPay {
    /// @notice Emitted when a payment is made
    /// @param asset The asset used for payment
    /// @param amount The amount of tokens used for payment
    /// @param onBehalfOf The identifier on whose behalf the payment was made
    /// @param merchant The merchant address
    /// @param memo Additional data or information about the payment
    /// @param amountInUSD The amount in USD
    /// @param sender The address that initiated the payment
    /// @param itemId The item ID
    event SendPayment(
        address indexed asset,
        uint248 amount,
        bytes32 onBehalfOf,
        address indexed merchant,
        bytes memo,
        uint248 amountInUSD,
        address indexed sender,
        bytes32 itemId
    );

    /// @notice Emitted when a payment is authorized
    /// @param transaction The transaction that was authorized
    /// @param transactionHash The hash of the transaction
    /// @param onBehalfOf The identifier on whose behalf the payment is made
    /// @param memo Additional data or information about the payment
    /// @param itemId The item ID
    event Authorized(
        PendingPayment.Transaction transaction, bytes32 transactionHash, bytes32 onBehalfOf, bytes memo, bytes32 itemId
    );

    /// @notice Emitted when an authorized payment is settled
    /// @param sourceAsset The source asset of the payment
    /// @param sourceAssetAmount The amount of source asset that was authorized
    /// @param payoutToken The token the merchant received
    /// @param receivedTargetAssetAmount The amount received by the merchant in payout token
    /// @param receivedRefundAmount The amount refunded to the client
    /// @param from The address that made the original payment
    /// @param merchant The merchant address
    /// @param transactionHash The transaction hash of the authorized payment
    event AuthorizedPaymentSettled(
        address indexed sourceAsset,
        uint248 sourceAssetAmount,
        address indexed payoutToken,
        uint256 receivedTargetAssetAmount,
        uint248 receivedRefundAmount,
        address indexed from,
        address merchant,
        bytes32 transactionHash
    );

    /// @notice Sets the payment asset
    /// @param assetAddress The asset to set
    /// @param paymentAsset AssetManagement.PaymentAsset struct
    /// @param path The path for the source asset to swap to USD (sourceAsset => USD)
    function setPaymentAsset(
        address assetAddress,
        AssetManagement.PaymentAsset calldata paymentAsset,
        bytes calldata path
    ) external;

    /// @notice Removes an asset from the payment assets
    /// @param asset The asset to remove
    function removePaymentAsset(address asset) external;

    /// @notice Gets the payment asset
    /// @param asset The asset to get
    /// @return paymentAsset The payment asset
    function getPaymentAsset(address asset) external view returns (AssetManagement.PaymentAsset memory paymentAsset);

    /// @notice Allows for sending ERC20 tokens to a target address
    /// @param asset The address of the ERC20 token to send
    /// @param amount The amount of tokens to send
    /// @param onBehalfOf The identifier on whose behalf the payment is made
    /// @param merchant The merchant address
    /// @param memo Additional data or information about the payment
    /// @param itemId The item ID
    function send(
        address asset,
        uint248 amount,
        bytes32 onBehalfOf,
        address merchant,
        bytes calldata memo,
        bytes32 itemId
    ) external;

    /// @notice Allows for sending ERC20 tokens with a custom swap path override
    /// @param customSourceAssetPath The custom swap path to override the stored sourceAssetPaths mapping
    /// @param amount The amount of tokens to send
    /// @param onBehalfOf The identifier on whose behalf the payment is made
    /// @param merchant The merchant address
    /// @param memo Additional data or information about the payment
    /// @param itemId The item ID
    function sendPathOverride(
        bytes calldata customSourceAssetPath,
        uint248 amount,
        bytes32 onBehalfOf,
        address merchant,
        bytes calldata memo,
        bytes32 itemId
    ) external;

    /// @notice Allows for sending ERC20 tokens with a custom swap path override and a callback contract
    /// @param customSourceAssetPath The custom swap path to override the stored sourceAssetPaths mapping
    /// @param amount The amount of tokens to send
    /// @param onBehalfOf The identifier on whose behalf the payment is made
    /// @param merchant The merchant address
    /// @param memo Additional data or information about the payment
    /// @param itemId The item ID, this is used to identify the callback contract
    /// @param callbackData The data to send to the callback contract
    function sendWithCallbackPathOverride(
        bytes calldata customSourceAssetPath,
        uint248 amount,
        bytes32 onBehalfOf,
        address merchant,
        bytes calldata memo,
        bytes32 itemId,
        bytes calldata callbackData
    ) external;

    /// @notice Allows for sending ERC20 tokens to a target address with a callback contract
    /// @param asset The address of the ERC20 token to send
    /// @param amount The amount of tokens to send
    /// @param onBehalfOf The identifier on whose behalf the payment is made
    /// @param merchant The merchant address
    /// @param memo Additional data or information about the payment
    /// @param itemId The item ID, this is used to identify the callback contract
    /// @param callbackData The data to send to the callback contract
    function sendWithCallback(
        address asset,
        uint248 amount,
        bytes32 onBehalfOf,
        address merchant,
        bytes calldata memo,
        bytes32 itemId,
        bytes calldata callbackData
    ) external;

    /// @notice Authorizes a payment to a target merchant
    /// the payment will be pulled from `msg.sender` and held pending in DSpay contract
    /// the payment is accounted for `onBehalfOf` but any refunded amount will be send to `msg.sender`
    /// @param asset The address of the ERC20 token to send
    /// @param amount The amount of tokens to send
    /// @param onBehalfOf The identifier on whose behalf the payment is made
    /// @param merchant The merchant address
    /// @param memo Additional data or information about the payment
    /// @param itemId The item ID
    function authorize(
        address asset,
        uint248 amount,
        bytes32 onBehalfOf,
        address merchant,
        bytes calldata memo,
        bytes32 itemId
    ) external;

    /// @notice Authorizes a payment to a target merchant with a callback contract
    /// the payment will be pulled from `msg.sender` and held pending in DSpay contract
    /// the payment is accounted for `onBehalfOf` but any refunded amount will be send to `msg.sender`
    /// @param asset The address of the ERC20 token to send
    /// @param amount The amount of tokens to send
    /// @param onBehalfOf The identifier on whose behalf the payment is made
    /// @param merchant The merchant address
    /// @param memo Additional data or information about the payment
    /// @param itemId The item ID, used to identify the callback contract
    /// @param callbackData The data to send to the callback contract
    function authorizeWithCallback(
        address asset,
        uint248 amount,
        bytes32 onBehalfOf,
        address merchant,
        bytes calldata memo,
        bytes32 itemId,
        bytes calldata callbackData
    ) external;

    /// @notice Sets the merchant configuration for the caller
    /// @param config Merchant configuration struct
    /// @param path The path for the target asset to swap to USD (USD => targetAsset)
    function setMerchantConfig(MerchantLogic.MerchantConfig calldata config, bytes calldata path) external;

    /// @notice Returns the merchant configuration for a given merchant
    /// @param merchant The merchant address
    /// @return config The merchant configuration
    function getMerchantConfig(address merchant) external view returns (MerchantLogic.MerchantConfig memory config);

    /// @notice Sets the price for an item
    /// @param item The item hash
    /// @param price The price in USD 18 decimals precision
    /// @dev msg.sender is the merchant address
    function setPaywallItemPrice(bytes32 item, uint248 price) external;

    /// @notice Gets the price for an item
    /// @param item The item hash
    /// @param merchant The merchant address
    /// @return price The price in USD 18 decimals precision
    function getPaywallItemPrice(bytes32 item, address merchant) external view returns (uint248 price);

    /// @notice Gets the executor address
    /// @return executor The executor address
    function getExecutorAddress() external view returns (address executor);

    /// @notice Gets the sequencer configuration
    /// @return sequencerConfig The current sequencer configuration
    function getSequencerConfig()
        external
        view
        returns (AssetManagement.AssetManagementSequencerConfig memory sequencerConfig);

    /// @notice Settles an authorized payment
    /// @param sourceAsset The source asset of the payment
    /// @param sourceAssetAmount The amount of source asset that was authorized
    /// @param from The address that made the original payment
    /// @param transactionHash The hash of the transaction to settle
    /// @param maxUsdValueOfTargetToken Maximum USD value allowed for the target token
    function settleAuthorizedPayment(
        address sourceAsset,
        uint248 sourceAssetAmount,
        address from,
        bytes32 transactionHash,
        uint248 maxUsdValueOfTargetToken
    ) external;

    /// @notice Settles an authorized payment
    /// @param customSourceAssetPath The custom swap path to override the stored sourceAssetPaths mapping
    /// @param sourceAssetAmount The amount of source asset that was authorized
    /// @param from The address that made the original payment
    /// @param transactionHash The hash of the transaction to settle
    /// @param maxUsdValueOfTargetToken Maximum USD value allowed for the target token
    function settleAuthorizedPaymentPathOverride(
        bytes calldata customSourceAssetPath,
        uint248 sourceAssetAmount,
        address from,
        bytes32 transactionHash,
        uint248 maxUsdValueOfTargetToken
    ) external;

    /// @notice Sets callback configuration for an item ID
    /// @param itemId The item ID
    /// @param config The callback configuration
    function setItemIdCallbackConfig(bytes32 itemId, MerchantLogic.ItemIdCallbackConfig calldata config) external;

    /// @notice Gets callback configuration for an item ID
    /// @param merchant The merchant address
    /// @param itemId The item ID
    /// @return config The callback configuration
    function getItemIdCallbackConfig(address merchant, bytes32 itemId)
        external
        view
        returns (MerchantLogic.ItemIdCallbackConfig memory config);
}
"
    },
    "src/libraries/AssetManagement.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;

import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
import {ISequencerUptimeFeed} from "../interfaces/ISequencerUptimeFeed.sol";
import {Utils} from "./Utils.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ZERO_ADDRESS} from "./Constants.sol";
/// @title AssetManagement
/// @notice Library for managing payment assets,
/// @dev It allows for setting, removing and getting payment assets.

library AssetManagement {
    using SafeERC20 for IERC20;

    /// @notice Error thrown when the price feed is invalid
    error InvalidPriceFeed();
    /// @notice Error thrown when the asset is not found
    error AssetNotFound();
    /// @notice Error thrown when the price feed data is invalid
    error InvalidPriceFeedData();
    /// @notice Error thrown when the price feed data is stale
    error StalePriceFeedData();
    /// @notice Error thrown when the asset is not supported for this method
    error AssetIsNotSupportedForThisMethod();
    /// @notice Error thrown when the L2 sequencer is down
    error SequencerDown();
    /// @notice Error thrown when the sequencer uptime feed grace period has not passed
    error GracePeriodNotOver();

    /// @notice Emitted when a new asset is added
    /// @param asset The asset address
    /// @param priceFeed The price feed address
    /// @param tokenDecimals The token decimals
    /// @param stalePriceThresholdInSeconds The stale price threshold in seconds
    event AssetAdded(
        address indexed asset, address priceFeed, uint8 tokenDecimals, uint64 stalePriceThresholdInSeconds
    );

    /// @notice Emitted when an asset is removed
    /// @param asset The asset address
    event AssetRemoved(address asset);

    /**
     * @notice Defines payment asset configuration within the DSpay protocol.
     */
    struct PaymentAsset {
        /// @notice  Price oracle
        address priceFeed;
        /// @notice  token decimals, added here in case erc20 token isn't fully compliant and doesn't expose that method.
        uint8 tokenDecimals;
        /// @notice threshold for price feed data in seconds
        uint64 stalePriceThresholdInSeconds;
        /// @notice whether the asset is active for new payments
        bool active;
    }

    /**
     * @notice Global configuration for asset management sequencer
     */
    struct AssetManagementSequencerConfig {
        /// @notice Chainlink L2 sequencer uptime feed address
        address sequencerUptimeFeed;
        /// @notice Grace period in seconds after sequencer is back up
        uint256 gracePeriod;
    }

    /// @notice Checks L2 sequencer uptime before allowing price feed operations
    /// @param sequencerConfig asset management sequencer configuration
    function _checkSequencerUptime(AssetManagementSequencerConfig storage sequencerConfig) internal view {
        if (sequencerConfig.sequencerUptimeFeed == ZERO_ADDRESS) return;

        // slither-disable-next-line unused-return
        (, int256 answer, uint256 startedAt,,) =
            ISequencerUptimeFeed(sequencerConfig.sequencerUptimeFeed).latestRoundData();

        bool isSequencerUp = answer == 0;
        if (!isSequencerUp) {
            revert SequencerDown();
        }

        // slither-disable-next-line timestamp
        if (startedAt != 0 && block.timestamp - startedAt < sequencerConfig.gracePeriod) {
            revert GracePeriodNotOver();
        }
    }

    /// @notice Validates the price feed
    /// @param paymentAsset payment asset
    function _validatePriceFeed(PaymentAsset memory paymentAsset) internal view {
        if (paymentAsset.priceFeed == ZERO_ADDRESS || !Utils.isContract(paymentAsset.priceFeed)) {
            revert InvalidPriceFeed();
        }

        // slither-disable-next-line unused-return
        (, int256 answer, uint256 startedAt, uint256 updatedAt,) =
            AggregatorV3Interface(paymentAsset.priceFeed).latestRoundData();

        // Check for invalid or incomplete price data
        if (answer <= 0 || startedAt == 0) {
            revert InvalidPriceFeedData();
        }

        // slither-disable-next-line timestamp
        if (updatedAt + paymentAsset.stalePriceThresholdInSeconds < block.timestamp) {
            revert StalePriceFeedData();
        }
    }

    /// @notice Sets the payment asset
    /// @param _assets assets mapping
    /// @param asset token address
    /// @param paymentAsset payment asset to set
    function set(
        mapping(address asset => PaymentAsset) storage _assets,
        address asset,
        PaymentAsset memory paymentAsset
    ) internal {
        _validatePriceFeed(paymentAsset);

        _assets[asset] = paymentAsset;
        emit AssetAdded(
            asset, paymentAsset.priceFeed, paymentAsset.tokenDecimals, paymentAsset.stalePriceThresholdInSeconds
        );
    }

    /// @notice Disables an asset for new payments while preserving configuration for existing authorized payments
    /// @param _assets assets mapping
    /// @param asset token address
    function remove(mapping(address asset => PaymentAsset) storage _assets, address asset) internal {
        if (!exists(_assets, asset)) {
            revert AssetNotFound();
        }
        _assets[asset].active = false;
        emit AssetRemoved(asset);
    }

    /// @notice Gets the payment asset
    /// @param _assets assets mapping
    /// @param assetAddress token address
    /// @return asset
    function get(mapping(address asset => PaymentAsset) storage _assets, address assetAddress)
        internal
        view
        returns (PaymentAsset storage asset)
    {
        asset = _assets[assetAddress];
        if (asset.priceFeed == ZERO_ADDRESS) revert AssetNotFound();
    }

    /// @notice Checks if an asset is supported for new payments
    /// @param _assets assets mapping
    /// @param assetAddress token address
    /// @return true if the asset exists and is active, false otherwise
    function isSupported(mapping(address asset => PaymentAsset) storage _assets, address assetAddress)
        internal
        view
        returns (bool)
    {
        return _assets[assetAddress].priceFeed != ZERO_ADDRESS && _assets[assetAddress].active;
    }

    /// @notice Checks if an asset exists in the mapping (regardless of active status)
    /// @param _assets assets mapping
    /// @param assetAddress token address
    /// @return true if the asset exists, false otherwise
    function exists(mapping(address asset => PaymentAsset) storage _assets, address assetAddress)
        internal
        view
        returns (bool)
    {
        return _assets[assetAddress].priceFeed != ZERO_ADDRESS;
    }

    /// @notice Gets the price of an asset
    /// @param _assets assets mapping
    /// @param assetAddress token address
    /// @param sequencerConfig asset management sequencer configuration
    /// @return safePrice price of the asset
    /// @return priceFeedDecimals price feed decimals
    function _getPrice(
        mapping(address asset => PaymentAsset) storage _assets,
        AssetManagementSequencerConfig storage sequencerConfig,
        address assetAddress
    ) internal view returns (uint256 safePrice, uint8 priceFeedDecimals) {
        _checkSequencerUptime(sequencerConfig);
        PaymentAsset storage paymentAsset = _assets[assetAddress];
        AggregatorV3Interface priceFeedContract = AggregatorV3Interface(paymentAsset.priceFeed);

        // slither-disable-next-line unused-return
        (, int256 price, uint256 startedAt, uint256 updatedAt,) = priceFeedContract.latestRoundData();

        // Check for invalid or incomplete price data
        if (price <= 0 || startedAt == 0) {
            revert InvalidPriceFeedData();
        }

        // slither-disable-next-line timestamp
        if (block.timestamp > updatedAt + paymentAsset.stalePriceThresholdInSeconds) {
            revert StalePriceFeedData();
        }

        safePrice = uint256(price);
        priceFeedDecimals = priceFeedContract.decimals();
    }

    /**
     * @dev Internal helper function to convert a token amount to its equivalent USD value.
     * @param _assets assets mapping
     * @param tokenAmount The amount of the token (in its smallest unit, e.g., wei for ETH).
     * @param assetAddress The address of the asset to convert.
     * @param sequencerConfig asset management sequencer configuration
     * @return usdValue The equivalent USD value in 18 decimal places.
     * @dev This function could revert if `tokenAmount * safePrice` overflows uint248
     */
    function convertToUsd(
        mapping(address asset => PaymentAsset) storage _assets,
        AssetManagementSequencerConfig storage sequencerConfig,
        address assetAddress,
        uint248 tokenAmount
    ) internal view returns (uint248 usdValue) {
        PaymentAsset storage paymentAsset = _assets[assetAddress];
        (uint256 safePrice, uint8 priceFeedDecimals) = _getPrice(_assets, sequencerConfig, assetAddress);

        if (paymentAsset.tokenDecimals + priceFeedDecimals > 18) {
            usdValue = uint248((tokenAmount * safePrice) / 10 ** (paymentAsset.tokenDecimals + priceFeedDecimals - 18));
        } else {
            usdValue = uint248((tokenAmount * safePrice) * 10 ** (18 - paymentAsset.tokenDecimals - priceFeedDecimals));
        }
    }

    /**
     * @dev Internal helper function to convert a USD value to its equivalent token amount.
     * @param usdValue The USD value to convert in 18 decimals.
     * @param asset The address of the asset to convert.
     * @param sequencerConfig asset management sequencer configuration
     * @return tokenAmount The equivalent token amount.
     */
    function convertUsdToToken(
        mapping(address asset => PaymentAsset) storage _assets,
        AssetManagementSequencerConfig storage sequencerConfig,
        address asset,
        uint248 usdValue
    ) internal view returns (uint248 tokenAmount) {
        (uint256 safePrice, uint8 priceFeedDecimals) = _getPrice(_assets, sequencerConfig, asset);

        uint248 adjustedPrice = uint248(safePrice) * uint248(10 ** (18 - priceFeedDecimals)); // price in 18 decimals
        tokenAmount = (usdValue * uint248(10 ** _assets[asset].tokenDecimals)) / adjustedPrice;
    }

    /// @notice Transfers asset to recipient and returns actual amount received
    /// @param asset The address of the asset to transfer
    /// @param amount The amount to transfer
    /// @param recipient The recipient of the transfer
    /// @return amountReceived The amount actually received by recipient
    function transferAsset(address asset, uint248 amount, address recipient) internal returns (uint248 amountReceived) {
        if (amount == 0) return 0;
        IERC20 token = IERC20(asset);
        uint256 beforeBalance = token.balanceOf(recipient);
        token.safeTransfer(recipient, amount);
        uint256 afterBalance = token.balanceOf(recipient);
        amountReceived = uint248(afterBalance - beforeBalance);
    }

    /// @notice Transfers asset from `from` to `recipient` and returns actual amount received
    /// @param asset The address of the asset to transfer
    /// @param amount The amount to transfer
    /// @param recipient The recipient of the transfer
    /// @return amountReceived The amount actually received by recipient
    function transferAssetFromCaller(address asset, uint248 amount, address recipient)
        internal
        returns (uint248 amountReceived)
    {
        if (amount == 0) return 0;
        IERC20 token = IERC20(asset);
        uint256 beforeBalance = token.balanceOf(recipient);
        token.safeTransferFrom(msg.sender, recipient, amount);
        uint256 afterBalance = token.balanceOf(recipient);
        amountReceived = uint248(afterBalance - beforeBalance);
    }
}
"
    },
    "src/libraries/MerchantLogic.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;

import {ZERO_ADDRESS} from "./Constants.sol";

library MerchantLogic {
    /// @notice Emitted when a merchant updates their configuration
    /// @param merchant Address of the merchant
    /// @param payoutAddresses Array of payout addresses
    /// @param payoutPercentages Array of payout percentages
    /// @param allowUserCustomPath Whether users can provide custom swap paths
    event MerchantConfigSet(
        address indexed merchant, address[] payoutAddresses, uint32[] payoutPercentages, bool allowUserCustomPath
    );

    /// @notice Error thrown when payout percentages don't sum to 100%
    error InvalidPayoutPercentageSum();

    /// @notice Error thrown when payout address is zero
    error PayoutAddressCannotBeZero();

    /// @notice Error thrown when no payout recipients are provided
    error NoPayoutRecipients();

    /// @notice Error thrown when payout address and percentage arrays have different lengths
    error PayoutArrayLengthMismatch();

    /// @notice Error thrown when a payout percentage is zero
    error ZeroPayoutPercentage();

    /// @notice Error thrown when too many payout recipients are specified
    error TooManyPayoutRecipients();

    error UnsupportedPaymentMethod();

    /// @notice Error thrown when item ID callback is not configured
    error ItemIdCallbackNotConfigured();

    /// @notice Error thrown when user provides custom path but merchant doesn't allow it
    error UserCustomPathNotAllowed();

    uint32 public constant PERCENTAGE_PRECISION = 1e6;
    uint32 public constant TOTAL_PERCENTAGE = 100 * PERCENTAGE_PRECISION;
    uint256 public constant MAX_PAYOUT_RECIPIENTS = 16;

    struct MerchantConfig {
        bool allowUserCustomPath;
        address[] payoutAddresses;
        uint32[] payoutPercentages;
    }

    struct MerchantLogicStorage {
        mapping(address merchantAddress => MerchantLogic.MerchantConfig merchantConfig) merchantConfigs;
        mapping(bytes32 => MerchantLogic.ItemIdCallbackConfig) itemIdCallbackConfigs;
    }

    bytes1 public constant SEND_METHOD = bytes1(uint8(1 << 0));
    bytes1 public constant AUTHORIZE_METHOD = bytes1(uint8(1 << 1));

    struct ItemIdCallbackConfig {
        bytes4 funcSig;
        address contractAddress;
        bool includePaymentMetadata;
        bytes1 acceptedMethods;
    }

    function setConfig(
        MerchantLogicStorage storage merchantLogicStorage,
        address merchant,
        MerchantConfig memory config
    ) internal {
        uint256 len = config.payoutAddresses.length;
        if (len == 0) {
            revert NoPayoutRecipients();
        }
        if (len > MAX_PAYOUT_RECIPIENTS) {
            revert TooManyPayoutRecipients();
        }
        if (len != config.payoutPercentages.length) {
            revert PayoutArrayLengthMismatch();
        }
        uint32 totalPercentage = 0;
        for (uint256 i = 0; i < len; ++i) {
            uint32 percentage = config.payoutPercentages[i];
            if (config.payoutAddresses[i] == ZERO_ADDRESS) {
                revert PayoutAddressCannotBeZero();
            }
            if (percentage == 0) {
                revert ZeroPayoutPercentage();
            }
            totalPercentage += percentage;
        }

        if (totalPercentage != TOTAL_PERCENTAGE) {
            revert InvalidPayoutPercentageSum();
        }

        merchantLogicStorage.merchantConfigs[merchant] = config;

        emit MerchantConfigSet(merchant, config.payoutAddresses, config.payoutPercentages, config.allowUserCustomPath);
    }

    function getConfig(MerchantLogicStorage storage merchantLogicStorage, address merchant)
        internal
        view
        returns (MerchantConfig memory config)
    {
        config = merchantLogicStorage.merchantConfigs[merchant];
    }

    function setItemIdCallback(
        MerchantLogicStorage storage merchantLogicStorage,
        address merchant,
        bytes32 itemId,
        ItemIdCallbackConfig memory callbackConfig
    ) internal {
        if (callbackConfig.acceptedMethods == 0) {
            revert UnsupportedPaymentMethod();
        }
        if (callbackConfig.acceptedMethods & ~(SEND_METHOD | AUTHORIZE_METHOD) != 0) {
            revert UnsupportedPaymentMethod();
        }
        merchantLogicStorage.itemIdCallbackConfigs[keccak256(abi.encodePacked(merchant, itemId))] = callbackConfig;
    }

    function getItemIdCallback(MerchantLogicStorage storage merchantLogicStorage, address merchant, bytes32 itemId)
        internal
        view
        returns (ItemIdCallbackConfig memory callbackConfig)
    {
        callbackConfig = merchantLogicStorage.itemIdCallbackConfigs[keccak256(abi.encodePacked(merchant, itemId))];

        if (callbackConfig.contractAddress == ZERO_ADDRESS) {
            revert ItemIdCallbackNotConfigured();
        }
    }
}
"
    },
    "src/libraries/SwapLogic.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ISwapRouter} from "../interfaces/ISwapRouter.sol";
import {ZERO_ADDRESS} from "./Constants.sol";

/// @title SwapLogic
/// @dev Library for swapping assets
library SwapLogic {
    using SafeERC20 for IERC20;

    uint256 internal constant WORD_SIZE = 0x20;
    uint256 internal constant ADDRESS_SIZE = 20;
    uint256 internal constant FEE_SIZE = 3;
    uint256 internal constant ADDRESS_PLUS_FEE = 23;
    uint256 internal constant ADDRESS_OFFSET_BITS = 96;
    u

Tags:
ERC20, ERC165, Multisig, Swap, Upgradeable, Multi-Signature, Factory, Oracle|addr:0x880a88bf31800ab4b48acd46220d9ba6898bb419|verified:true|block:23685449|tx:0x9401335001e0fb57af02db76fc61cd502ca578c7e5fd79126c916a1e22ddd008|first_check:1761770131

Submitted on: 2025-10-29 21:35:32

Comments

Log in to comment.

No comments yet.