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
Submitted on: 2025-10-29 21:35:32
Comments
Log in to comment.
No comments yet.