Repository: coinbase/commerce-onchain-payment-protocol
Branch: master
Commit: d891289bd1f4
Files: 8
Total size: 62.8 KB
Directory structure:
gitextract_2euvt6jp/
├── .gitignore
├── LICENSE.md
├── README.md
└── contracts/
├── interfaces/
│ ├── IERC7597.sol
│ ├── ITransfers.sol
│ └── IWrappedNativeCurrency.sol
├── transfers/
│ └── Transfers.sol
└── utils/
└── Sweepable.sol
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
.idea
================================================
FILE: LICENSE.md
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2023 Coinbase, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
# Coinbase Commerce Onchain Payment Protocol
The Coinbase Commerce Onchain Payment Protocol allows payers and merchants to transact using the blockchain as a settlement layer and source of truth.
It provides the following benefits over "traditional" cryptocurrency payments:
- Guaranteed settlement: merchants always receive exactly the amount that they request.
- Automatic conversion: payers can pay with any token that has liquidity on Uniswap, without exposing merchants to price volatility.
- Removal of payment errors: it is no longer possible to pay the wrong amount or to the wrong address.
### Contract Deployments
As of July 31, 2024, the Commerce Onchain Payment Protocol is deployed in the following locations:
| Chain | Environment | Address |
| -------- | --------------- | -------------------------------------------- |
| Ethereum | Mainnet | `0x1DAe28D7007703196d6f456e810F67C33b51b25C` |
| Ethereum | Sepolia Testnet | `0x96A08D8e8631b6dB52Ea0cbd7232d9A85d239147` |
| Polygon | Mainnet | `0xc2252Ce3348B8dAf90583E53e07Be53d3aE728FB` |
| Polygon | Amoy Testnet | `0x1A8f790a10D26bAd97dB8Da887D212eA49461cCC` |
| Base | Mainnet | `0xeADE6bE02d043b3550bE19E960504dbA14A14971` |
| Base | Sepolia Testnet | `0x96A08D8e8631b6dB52Ea0cbd7232d9A85d239147` |
Since the contract is non-upgradeable, these addresses will change when new
versions are deployed.
### Browsing this Repo
The core source code can be found in [Transfers.sol](contracts/transfers/Transfers.sol).
Excluded from this repo is a copy of [Uniswap/permit2](https://github.com/Uniswap/permit2),
which would be copied to `contracts/permit2` in order to compile.
## Overview
### Operators
The Transfers contract facilitates payments from a payer to a merchant. Before
it may be used, an "operator" must register with the contract and specify
a destination for fees. This operator is responsible for setting merchants up
with the protocol and providing a UI for both merchants and payers to interact
with it. Registering as an operator is permissionless, and Coinbase maintains
control of an address used as the operator for Coinbase Commerce.
### Transfer Intents
Once an operator is registered, they may begin facilitating payments. Individual
payments use a primitive called a `TransferIntent`, represented by a Solidity
struct of the same name. This struct specifies the following:
- The merchant's address
- The currency the merchant wishes to receive
- The amount of that currency the merchant wishes to receive
- The deadline by which the payment must be made
- The payer's address
- The chain the payer will pay on
- The address any refund should be directed to
- The operator who is facilitating the payment
- The fee the operator should receive
- A unique identifier for identifying the payment
- A signature (and optional signature prefix) from the operator
Along with these attributes, a `TransferIntent` must be signed by the operator.
This allows an operator to be selective about what payments to allow based on
internal policies, legal requirements, or other reasons. It also ensures that
a `TransferIntent` cannot be forged or have its data modified in any way.
### Contract Guarantees
The contract ensures that, for a given valid `TransferIntent`:
- The merchant always receives the exact amount requested
- The merchant never receives payments past a stated deadline
- The merchant never receives more than one payment
- Payments may be made using the merchant's requested currency, or swapped from
another token as part of the payment transaction
- Unsuccessful or partial payments will never reach the merchant, thus
guaranteeing that payments are atomic. Either the merchant is correctly paid
in full and the fee is correctly charged, or the transaction reverts and no
state is changed onchain.
### Contract payment methods
Depending on the settlement token and the input token, along with the way
in which the payer allows movement of their input token, a frontend must select
the appropriate method by which to pay a `TransferIntent`. These methods are:
- `transferNative`: The merchant wants ETH and the payer wants to pay ETH
- `transferToken`: The merchant wants a token and the payer wants to pay with
that token. Uses Permit2 for token movement.
- `transferTokenPreApproved`: Same as `transferToken`, except the Transfers
contract is directly approved by the payer for the payment token
- `wrapAndTransfer`: The merchant wants WETH and the payer wants to pay ETH
- `unwrapAndTransfer`: The merchant wants ETH and the payer wants to pay WETH
- `unwrapAndTransferPreApproved`: Same as `unwrapAndTransfer`, except the
Transfers contract is directly approved by the payer for WETH
- `swapAndTransferUniswapV3Native`: The merchant wants a token and the payer
wants to pay ETH. The token must have sufficient liquidity with ETH on Uniswap
V3.
- `swapAndTransferUniswapV3Token`: The merchant wants either ETH or a token and
the payer wants to pay with a different token. The payment token must have
sufficient liquidity with the settlement token on Uniswap V3.
- `swapAndTransferUniswapV3TokenPreApproved`: Same as
`swapAndTransferUniswapV3Token`, except the Transfers contract is directly
approved by the payer for the payment token
For any EVM-compatible network where ETH is not the native/gas currency, the
above descriptions should substitute that currency. For example, payments on
Polygon would use MATIC in the above descriptions.
### Payment Transaction Results
When the payment is successful, a `Transferred` event is emitted by the contract
with details about:
- The operator address
- The unique id of the `TransferIntent`
- The merchant (recipient) address
- The payer (sender) address
- The input token that was spent by the payer
- The amount of the input token spent by the payer
In the case of errors, a specific error type is returned with details about what
went wrong.
================================================
FILE: contracts/interfaces/IERC7597.sol
================================================
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;
import "@openzeppelin/contracts/interfaces/IERC2612.sol";
interface IERC7597 is IERC2612 {
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
bytes memory signature
) external;
}
================================================
FILE: contracts/interfaces/ITransfers.sol
================================================
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;
import "../permit2/src/interfaces/ISignatureTransfer.sol";
// @notice Description of the transfer
// @member recipientAmount Amount of currency to transfer
// @member deadline The timestamp by when the transfer must be in a block.
// @member chainId The chain which the transfer must occur on.
// @member recipient The address which will receive the funds.
// @member recipientCurrency The currency address that amount is priced in.
// @member refundDestination The address which will receive any refunds. If blank, this will be msg.sender.
// @member feeAmount The fee value (in currency) to send to the operator.
// @member id An ID which can be used to track payments.
// @member operator The address of the operator (who created and signed the intent).
// @member signature A hash of all the other struct properties signed by the operator.
// @member prefix An alternate signature prefix to use instead of the standard EIP-191 "\x19Ethereum Signed Message:\n"
// @dev signature=keccak256(encodePacked(...allPropsInOrderExceptSignatureAndPrefix, chainId, _msgSender(), address(transfersContract))
struct TransferIntent {
uint256 recipientAmount;
uint256 deadline;
address payable recipient;
address recipientCurrency;
address refundDestination;
uint256 feeAmount;
bytes16 id;
address operator;
bytes signature;
bytes prefix;
}
struct Permit2SignatureTransferData {
ISignatureTransfer.PermitTransferFrom permit;
ISignatureTransfer.SignatureTransferDetails transferDetails;
bytes signature;
}
struct EIP2612SignatureTransferData {
address owner; // The owner of the funds
bytes signature; // The signature for the permit
}
// @title Transfers Contract
// @notice Functions for making checked transfers between accounts
interface ITransfers {
// @notice Emitted when a transfer is completed
// @param operator The operator for the transfer intent
// @param id The ID of the transfer intent
// @param recipient Who recieved the funds.
// @param sender Who sent the funds.
// @param spentAmount How much the payer sent
// @param spentCurrency What currency the payer sent
event Transferred(
address indexed operator,
bytes16 id,
address recipient,
address sender,
uint256 spentAmount,
address spentCurrency
);
// @notice Raised when a native currency transfer fails
// @param recipient Who the transfer was intended for
// @param amount The amount of the transfer
// @param isRefund Whether the transfer was part of a refund
// @param data The data returned from the failed call
error NativeTransferFailed(address recipient, uint256 amount, bool isRefund, bytes data);
// @notice Emitted when an operator is registered
// @param operator The operator that was registered
// @param feeDestination The new fee destination for the operator
event OperatorRegistered(address operator, address feeDestination);
// @notice Emitted when an operator is unregistered
// @param operator The operator that was registered
event OperatorUnregistered(address operator);
// @notice Raised when the operator in the intent is not registered
error OperatorNotRegistered();
// @notice Raised when the intent signature is invalid
error InvalidSignature();
// @notice Raised when the invalid amount of native currency is provided
// @param difference The surplus (or deficit) amount sent
error InvalidNativeAmount(int256 difference);
// @notice Raised when the payer does not have enough of the payment token
// @param difference The balance deficit
error InsufficientBalance(uint256 difference);
// @notice Raised when the payer has not approved enough of the payment token
// @param difference The allowance deficit
error InsufficientAllowance(uint256 difference);
// @notice Raised when providing an intent with the incorrect currency. e.g. a USDC intent to `wrapAndTransfer`
// @param attemptedCurrency The currency the payer attempted to pay with
error IncorrectCurrency(address attemptedCurrency);
// @notice Raised when the permit2 transfer details are incorrect
error InvalidTransferDetails();
// @notice Raised when an intent is paid past its deadline
error ExpiredIntent();
// @notice Raised when an intent's recipient is the null address
error NullRecipient();
// @notice Raised when an intent has already been processed
error AlreadyProcessed();
// @notice Raised when a transfer does not result in the correct balance increase,
// such as with fee-on-transfer tokens
error InexactTransfer();
// @notice Raised when a swap fails and returns a reason string
// @param reason The error reason returned from the swap
error SwapFailedString(string reason);
// @notice Raised when a swap fails and returns another error
// @param reason The error reason returned from the swap
error SwapFailedBytes(bytes reason);
// @notice Send the exact amount of the native currency from the sender to the recipient.
// @dev The intent's recipient currency must be the native currency.
// @param _intent The intent which describes the transfer
function transferNative(TransferIntent calldata _intent) external payable;
// @notice Transfer the exact amount of any ERC-20 token from the sender to the recipient.
// @dev The intent's recipient currency must be an ERC-20 token matching the one in `_signatureTransferData`.
// @dev The user must have approved the Permit2 contract for at least `_intent.recipientAmount + _intent.feeAmount`
// with the `_intent.recipientCurrency` ERC-20 contract prior to invoking.
// @param _intent The intent which describes the transfer
function transferToken(
TransferIntent calldata _intent,
Permit2SignatureTransferData calldata _signatureTransferData
) external;
// @notice Transfer the exact amount of any ERC-20 token from the sender to the recipient.
// @dev The intent's recipient currency must be an ERC-20 token.
// @dev The user must have approved this contract for at least `_intent.recipientAmount + _intent.feeAmount`
// with the `_intent.recipientCurrency` ERC-20 contract prior to invoking.
// @param _intent The intent which describes the transfer
function transferTokenPreApproved(TransferIntent calldata _intent) external;
// @notice Takes native currency (e.g. ETH) from the sender and sends wrapped currency (e.g. wETH) to the recipient.
// @dev The intent's recipient currency must be the wrapped native currency.
// @param _intent The intent which describes the transfer
function wrapAndTransfer(TransferIntent calldata _intent) external payable;
// @notice Takes wrapped currency (e.g. wETH) from the sender and sends native currency (e.g. ETH) to the recipient.
// @dev The intent's recipient currency must be the native currency.
// @dev The user must have approved the Permit2 contract for at least `_intent.recipientAmount + _intent.feeAmount`
// with the wETH contract prior to invoking.
// @param _intent The intent which describes the transfer
// @param _signatureTransferData The signed Permit2 transfer data for the payment
function unwrapAndTransfer(
TransferIntent calldata _intent,
Permit2SignatureTransferData calldata _signatureTransferData
) external;
// @notice Takes wrapped currency (e.g. wETH) from the sender and sends native currency (e.g. ETH) to the recipient.
// @dev The intent's recipient currency must be the native currency.
// @dev The user must have approved this contract for at least `_intent.recipientAmount + _intent.feeAmount` with the wETH contract prior to invoking.
// @param _intent The intent which describes the transfer
function unwrapAndTransferPreApproved(TransferIntent calldata _intent) external;
// @notice Allows the sender to pay for an intent with a swap from the native currency using Uniswap.
// @param _intent The intent which describes the transfer
// @param poolFeesTier The Uniswap pool fee the user wishes to pay. See: https://docs.uniswap.org/protocol/concepts/V3-overview/fees#pool-fees-tiers
function swapAndTransferUniswapV3Native(TransferIntent calldata _intent, uint24 poolFeesTier) external payable;
// @notice Allows the sender to pay for an intent with a swap from any ERC-20 token using Uniswap.
// @dev The user must have approved the Permit2 contract for at least `_signatureTransferData.transferDetails.requestedAmount`
// with the `_signatureTransferData.permit.permitted.token` ERC-20 contract prior to invoking.
// @param _intent The intent which describes the transfer
// @param _signatureTransferData The signed Permit2 transfer data for the payment
// @param poolFeesTier The Uniswap pool fee the user wishes to pay. See: https://docs.uniswap.org/protocol/concepts/V3-overview/fees#pool-fees-tiers
function swapAndTransferUniswapV3Token(
TransferIntent calldata _intent,
Permit2SignatureTransferData calldata _signatureTransferData,
uint24 poolFeesTier
) external;
// @notice Allows the sender to pay for an intent with a swap from any ERC-20 token using Uniswap.
// @dev The user must have approved this contract for at least `maxWillingToPay` with the `_tokenIn` ERC-20 contract prior to invoking.
// @param _intent The intent which describes the transfer
// @param _tokenIn The currency address which the sender wishes to pay for the intent.
// @param maxWillingToPay The maximum amount of _tokenIn the sender is willing to pay.
// @param poolFeesTier The Uniswap pool fee the user wishes to pay. See: https://docs.uniswap.org/protocol/concepts/V3-overview/fees#pool-fees-tiers
function swapAndTransferUniswapV3TokenPreApproved(
TransferIntent calldata _intent,
address _tokenIn,
uint256 maxWillingToPay,
uint24 poolFeesTier
) external;
// @notice Allows the sender to pay for an intent with gasless transaction
// @param _intent The intent which describes the transfer
// @param _signatureTransferData The signed EIP-2612 permit data for the payment
function subsidizedTransferToken(
TransferIntent calldata _intent,
EIP2612SignatureTransferData calldata _signatureTransferData
) external;
}
================================================
FILE: contracts/interfaces/IWrappedNativeCurrency.sol
================================================
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
// @title Represented wrapped (e.g. wETH) currencies
interface IWrappedNativeCurrency is IERC20 {
function deposit() external payable;
function withdraw(uint256) external;
}
================================================
FILE: contracts/transfers/Transfers.sol
================================================
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Context.sol";
import "@uniswap/universal-router/contracts/interfaces/IUniversalRouter.sol";
import {Commands as UniswapCommands} from "@uniswap/universal-router/contracts/libraries/Commands.sol";
import {Constants as UniswapConstants} from "@uniswap/universal-router/contracts/libraries/Constants.sol";
import "../interfaces/IWrappedNativeCurrency.sol";
import "../interfaces/ITransfers.sol";
import "../interfaces/IERC7597.sol";
import "../utils/Sweepable.sol";
import "../permit2/src/Permit2.sol";
// Uniswap error selectors, used to surface information when swaps fail
// Pulled from @uniswap/universal-router/out/V3SwapRouter.sol/V3SwapRouter.json after compiling with forge
bytes32 constant V3_INVALID_SWAP = keccak256(hex"316cf0eb");
bytes32 constant V3_TOO_LITTLE_RECEIVED = keccak256(hex"39d35496");
bytes32 constant V3_TOO_MUCH_REQUESTED = keccak256(hex"739dbe52");
bytes32 constant V3_INVALID_AMOUNT_OUT = keccak256(hex"d4e0248e");
bytes32 constant V3_INVALID_CALLER = keccak256(hex"32b13d91");
// @inheritdoc ITransfers
contract Transfers is Context, Ownable, Pausable, ReentrancyGuard, Sweepable, ITransfers {
using SafeERC20 for IERC20;
using SafeERC20 for IWrappedNativeCurrency;
// @dev Map of operator addresses and fee destinations.
mapping(address => address) private feeDestinations;
// @dev Map of operator addresses to a map of transfer intent ids that have been processed
mapping(address => mapping(bytes16 => bool)) private processedTransferIntents;
// @dev Represents native token of a chain (e.g. ETH or MATIC)
address private immutable NATIVE_CURRENCY = address(0);
// @dev Uniswap on-chain contract
IUniversalRouter private immutable uniswap;
// @dev permit2 SignatureTransfer contract address. Used for tranferring tokens with a signature instead of a full transaction.
// See: https://github.com/Uniswap/permit2
Permit2 public immutable permit2;
// @dev Canonical wrapped token for this chain. e.g. (wETH or wMATIC).
IWrappedNativeCurrency private immutable wrappedNativeCurrency;
// @param _uniswap The address of the Uniswap V3 swap router
// @param _wrappedNativeCurrency The address of the wrapped token for this chain
constructor(
IUniversalRouter _uniswap,
Permit2 _permit2,
address _initialOperator,
address _initialFeeDestination,
IWrappedNativeCurrency _wrappedNativeCurrency
) {
require(
address(_uniswap) != address(0) &&
address(_permit2) != address(0) &&
address(_wrappedNativeCurrency) != address(0) &&
_initialOperator != address(0) &&
_initialFeeDestination != address(0),
"invalid constructor parameters"
);
uniswap = _uniswap;
permit2 = _permit2;
wrappedNativeCurrency = _wrappedNativeCurrency;
// Sets an initial operator to enable immediate payment processing
feeDestinations[_initialOperator] = _initialFeeDestination;
}
// @dev Raises errors if the intent is invalid
// @param _intent The intent to validate
modifier validIntent(TransferIntent calldata _intent, address sender) {
bytes32 hash = keccak256(
abi.encodePacked(
_intent.recipientAmount,
_intent.deadline,
_intent.recipient,
_intent.recipientCurrency,
_intent.refundDestination,
_intent.feeAmount,
_intent.id,
_intent.operator,
block.chainid,
sender,
address(this)
)
);
bytes32 signedMessageHash;
if (_intent.prefix.length == 0) {
// Use 'default' message prefix.
signedMessageHash = ECDSA.toEthSignedMessageHash(hash);
} else {
// Use custom message prefix.
signedMessageHash = keccak256(abi.encodePacked(_intent.prefix, hash));
}
address signer = ECDSA.recover(signedMessageHash, _intent.signature);
if (signer != _intent.operator) {
revert InvalidSignature();
}
if (_intent.deadline < block.timestamp) {
revert ExpiredIntent();
}
if (_intent.recipient == address(0)) {
revert NullRecipient();
}
if (processedTransferIntents[_intent.operator][_intent.id]) {
revert AlreadyProcessed();
}
_;
}
// @dev Raises an error if the operator in the transfer intent is not registered.
// @param _intent The intent to validate
modifier operatorIsRegistered(TransferIntent calldata _intent) {
if (feeDestinations[_intent.operator] == address(0)) revert OperatorNotRegistered();
_;
}
modifier exactValueSent(TransferIntent calldata _intent) {
// Make sure the correct value was sent
uint256 neededAmount = _intent.recipientAmount + _intent.feeAmount;
if (msg.value > neededAmount) {
revert InvalidNativeAmount(int256(msg.value - neededAmount));
} else if (msg.value < neededAmount) {
revert InvalidNativeAmount(-int256(neededAmount - msg.value));
}
_;
}
// @inheritdoc ITransfers
function transferNative(TransferIntent calldata _intent)
external
payable
override
nonReentrant
whenNotPaused
validIntent(_intent, _msgSender())
operatorIsRegistered(_intent)
exactValueSent(_intent)
{
// Make sure the recipient wants the native currency
if (_intent.recipientCurrency != NATIVE_CURRENCY) revert IncorrectCurrency(NATIVE_CURRENCY);
if (msg.value > 0) {
// Complete the payment
transferFundsToDestinations(_intent);
}
succeedPayment(_intent, msg.value, NATIVE_CURRENCY, _msgSender());
}
// @inheritdoc ITransfers
function transferToken(
TransferIntent calldata _intent,
Permit2SignatureTransferData calldata _signatureTransferData
) external override nonReentrant whenNotPaused validIntent(_intent, _msgSender()) operatorIsRegistered(_intent) {
// Make sure the recipient wants a token and the payer is sending it
if (
_intent.recipientCurrency == NATIVE_CURRENCY ||
_signatureTransferData.permit.permitted.token != _intent.recipientCurrency
) {
revert IncorrectCurrency(_signatureTransferData.permit.permitted.token);
}
// Make sure the payer has enough of the payment token
IERC20 erc20 = IERC20(_intent.recipientCurrency);
uint256 neededAmount = _intent.recipientAmount + _intent.feeAmount;
uint256 payerBalance = erc20.balanceOf(_msgSender());
if (payerBalance < neededAmount) {
revert InsufficientBalance(neededAmount - payerBalance);
}
if (neededAmount > 0) {
// Make sure the payer is transferring the right amount to this contract
if (
_signatureTransferData.transferDetails.to != address(this) ||
_signatureTransferData.transferDetails.requestedAmount != neededAmount
) {
revert InvalidTransferDetails();
}
// Record our balance before (most likely zero) to detect fee-on-transfer tokens
uint256 balanceBefore = erc20.balanceOf(address(this));
// Transfer the payment token to this contract
permit2.permitTransferFrom(
_signatureTransferData.permit,
_signatureTransferData.transferDetails,
_msgSender(),
_signatureTransferData.signature
);
// Make sure this is not a fee-on-transfer token
revertIfInexactTransfer(neededAmount, balanceBefore, erc20, address(this));
// Complete the payment
transferFundsToDestinations(_intent);
}
succeedPayment(_intent, neededAmount, _intent.recipientCurrency, _msgSender());
}
// @inheritdoc ITransfers
function transferTokenPreApproved(TransferIntent calldata _intent)
external
override
nonReentrant
whenNotPaused
validIntent(_intent, _msgSender())
operatorIsRegistered(_intent)
{
// Make sure the recipient wants a token
if (_intent.recipientCurrency == NATIVE_CURRENCY) {
revert IncorrectCurrency(_intent.recipientCurrency);
}
// Make sure the payer has enough of the payment token
IERC20 erc20 = IERC20(_intent.recipientCurrency);
uint256 neededAmount = _intent.recipientAmount + _intent.feeAmount;
uint256 payerBalance = erc20.balanceOf(_msgSender());
if (payerBalance < neededAmount) {
revert InsufficientBalance(neededAmount - payerBalance);
}
// Make sure the payer has approved this contract for a sufficient transfer
uint256 allowance = erc20.allowance(_msgSender(), address(this));
if (allowance < neededAmount) {
revert InsufficientAllowance(neededAmount - allowance);
}
if (neededAmount > 0) {
// Record our balance before (most likely zero) to detect fee-on-transfer tokens
uint256 balanceBefore = erc20.balanceOf(address(this));
// Transfer the payment token to this contract
erc20.safeTransferFrom(_msgSender(), address(this), neededAmount);
// Make sure this is not a fee-on-transfer token
revertIfInexactTransfer(neededAmount, balanceBefore, erc20, address(this));
// Complete the payment
transferFundsToDestinations(_intent);
}
succeedPayment(_intent, neededAmount, _intent.recipientCurrency, _msgSender());
}
// @inheritdoc ITransfers
// @dev Wraps msg.value into wrapped token and transfers to recipient.
function wrapAndTransfer(TransferIntent calldata _intent)
external
payable
override
nonReentrant
whenNotPaused
validIntent(_intent, _msgSender())
operatorIsRegistered(_intent)
exactValueSent(_intent)
{
// Make sure the recipient wants to receive the wrapped native currency
if (_intent.recipientCurrency != address(wrappedNativeCurrency)) {
revert IncorrectCurrency(NATIVE_CURRENCY);
}
if (msg.value > 0) {
// Wrap the sent native currency
wrappedNativeCurrency.deposit{value: msg.value}();
// Complete the payment
transferFundsToDestinations(_intent);
}
succeedPayment(_intent, msg.value, NATIVE_CURRENCY, _msgSender());
}
// @inheritdoc ITransfers
// @dev Requires _msgSender() to have approved this contract to use the wrapped token.
// @dev Unwraps into native token and transfers native token (e.g. ETH) to _intent.recipient.
function unwrapAndTransfer(
TransferIntent calldata _intent,
Permit2SignatureTransferData calldata _signatureTransferData
) external override nonReentrant whenNotPaused validIntent(_intent, _msgSender()) operatorIsRegistered(_intent) {
// Make sure the recipient wants the native currency and that the payer is
// sending the wrapped native currency
if (
_intent.recipientCurrency != NATIVE_CURRENCY ||
_signatureTransferData.permit.permitted.token != address(wrappedNativeCurrency)
) {
revert IncorrectCurrency(_signatureTransferData.permit.permitted.token);
}
// Make sure the payer has enough of the wrapped native currency
uint256 neededAmount = _intent.recipientAmount + _intent.feeAmount;
uint256 payerBalance = wrappedNativeCurrency.balanceOf(_msgSender());
if (payerBalance < neededAmount) {
revert InsufficientBalance(neededAmount - payerBalance);
}
if (neededAmount > 0) {
// Make sure the payer is transferring the right amount of the wrapped native currency to the contract
if (
_signatureTransferData.transferDetails.to != address(this) ||
_signatureTransferData.transferDetails.requestedAmount != neededAmount
) {
revert InvalidTransferDetails();
}
// Transfer the payer's wrapped native currency to the contract
permit2.permitTransferFrom(
_signatureTransferData.permit,
_signatureTransferData.transferDetails,
_msgSender(),
_signatureTransferData.signature
);
// Complete the payment
unwrapAndTransferFundsToDestinations(_intent);
}
succeedPayment(_intent, neededAmount, address(wrappedNativeCurrency), _msgSender());
}
// @inheritdoc ITransfers
// @dev Requires _msgSender() to have approved this contract to use the wrapped token.
// @dev Unwraps into native token and transfers native token (e.g. ETH) to _intent.recipient.
function unwrapAndTransferPreApproved(TransferIntent calldata _intent)
external
override
nonReentrant
whenNotPaused
validIntent(_intent, _msgSender())
operatorIsRegistered(_intent)
{
// Make sure the recipient wants the native currency
if (_intent.recipientCurrency != NATIVE_CURRENCY) {
revert IncorrectCurrency(address(wrappedNativeCurrency));
}
// Make sure the payer has enough of the wrapped native currency
uint256 neededAmount = _intent.recipientAmount + _intent.feeAmount;
uint256 payerBalance = wrappedNativeCurrency.balanceOf(_msgSender());
if (payerBalance < neededAmount) {
revert InsufficientBalance(neededAmount - payerBalance);
}
// Make sure the payer has approved this contract for a sufficient transfer
uint256 allowance = wrappedNativeCurrency.allowance(_msgSender(), address(this));
if (allowance < neededAmount) {
revert InsufficientAllowance(neededAmount - allowance);
}
if (neededAmount > 0) {
// Transfer the payer's wrapped native currency to the contract
wrappedNativeCurrency.safeTransferFrom(_msgSender(), address(this), neededAmount);
// Complete the payment
unwrapAndTransferFundsToDestinations(_intent);
}
succeedPayment(_intent, neededAmount, address(wrappedNativeCurrency), _msgSender());
}
/*------------------------------------------------------------------*\
| Swap and Transfer
\*------------------------------------------------------------------*/
// @inheritdoc ITransfers
function swapAndTransferUniswapV3Native(TransferIntent calldata _intent, uint24 poolFeesTier)
external
payable
override
nonReentrant
whenNotPaused
validIntent(_intent, _msgSender())
operatorIsRegistered(_intent)
{
// Make sure a swap is actually required, otherwise the payer should use `wrapAndTransfer` or `transferNative`
if (
_intent.recipientCurrency == NATIVE_CURRENCY || _intent.recipientCurrency == address(wrappedNativeCurrency)
) {
revert IncorrectCurrency(NATIVE_CURRENCY);
}
uint256 neededAmount = _intent.recipientAmount + _intent.feeAmount;
uint256 amountSwapped = 0;
if (neededAmount > 0) {
// Perform the swap
amountSwapped = swapTokens(_intent, address(wrappedNativeCurrency), msg.value, poolFeesTier);
}
// Complete the payment
succeedPayment(_intent, amountSwapped, NATIVE_CURRENCY, _msgSender());
}
// @inheritdoc ITransfers
function swapAndTransferUniswapV3Token(
TransferIntent calldata _intent,
Permit2SignatureTransferData calldata _signatureTransferData,
uint24 poolFeesTier
) external override nonReentrant whenNotPaused validIntent(_intent, _msgSender()) operatorIsRegistered(_intent) {
IERC20 tokenIn = IERC20(_signatureTransferData.permit.permitted.token);
// Make sure a swap is actually required
if (address(tokenIn) == _intent.recipientCurrency) {
revert IncorrectCurrency(address(tokenIn));
}
// Make sure the transfer is to this contract
if (_signatureTransferData.transferDetails.to != address(this)) {
revert InvalidTransferDetails();
}
uint256 neededAmount = _intent.recipientAmount + _intent.feeAmount;
uint256 maxWillingToPay = _signatureTransferData.transferDetails.requestedAmount;
uint256 amountSwapped = 0;
if (neededAmount > 0) {
// Record our balance before (most likely zero) to detect fee-on-transfer tokens
uint256 balanceBefore = tokenIn.balanceOf(address(this));
// Transfer the payer's tokens to this contract
permit2.permitTransferFrom(
_signatureTransferData.permit,
_signatureTransferData.transferDetails,
_msgSender(),
_signatureTransferData.signature
);
// Make sure this is not a fee-on-transfer token
revertIfInexactTransfer(maxWillingToPay, balanceBefore, tokenIn, address(this));
// Perform the swap
amountSwapped = swapTokens(_intent, address(tokenIn), maxWillingToPay, poolFeesTier);
}
// Complete the payment
succeedPayment(_intent, amountSwapped, address(tokenIn), _msgSender());
}
// @inheritdoc ITransfers
function swapAndTransferUniswapV3TokenPreApproved(
TransferIntent calldata _intent,
address _tokenIn,
uint256 maxWillingToPay,
uint24 poolFeesTier
) external override nonReentrant whenNotPaused validIntent(_intent, _msgSender()) operatorIsRegistered(_intent) {
IERC20 tokenIn = IERC20(_tokenIn);
// Make sure a swap is actually required
if (address(tokenIn) == _intent.recipientCurrency) {
revert IncorrectCurrency(address(tokenIn));
}
// Make sure the payer has enough of the payment token
uint256 payerBalance = tokenIn.balanceOf(_msgSender());
if (payerBalance < maxWillingToPay) {
revert InsufficientBalance(maxWillingToPay - payerBalance);
}
// Make sure the payer has approved this contract for a sufficient transfer
uint256 allowance = tokenIn.allowance(_msgSender(), address(this));
if (allowance < maxWillingToPay) {
revert InsufficientAllowance(maxWillingToPay - allowance);
}
uint256 neededAmount = _intent.recipientAmount + _intent.feeAmount;
uint256 amountSwapped = 0;
if (neededAmount > 0) {
// Record our balance before (most likely zero) to detect fee-on-transfer tokens
uint256 balanceBefore = tokenIn.balanceOf(address(this));
// Transfer the payment token to this contract
tokenIn.safeTransferFrom(_msgSender(), address(this), maxWillingToPay);
// Make sure this is not a fee-on-transfer token
revertIfInexactTransfer(maxWillingToPay, balanceBefore, tokenIn, address(this));
// Perform the swap
amountSwapped = swapTokens(_intent, address(tokenIn), maxWillingToPay, poolFeesTier);
}
// Complete the payment
succeedPayment(_intent, amountSwapped, address(tokenIn), _msgSender());
}
// @inheritdoc ITransfers
function subsidizedTransferToken(
TransferIntent calldata _intent,
EIP2612SignatureTransferData calldata _signatureTransferData
)
external
override
nonReentrant
whenNotPaused
validIntent(_intent, _signatureTransferData.owner)
operatorIsRegistered(_intent)
{
// Make sure the recipient wants a token
if (_intent.recipientCurrency == NATIVE_CURRENCY) {
revert IncorrectCurrency(_intent.recipientCurrency);
}
// Check the balance of the payer
IERC20 erc20 = IERC20(_intent.recipientCurrency);
uint256 neededAmount = _intent.recipientAmount + _intent.feeAmount;
uint256 payerBalance = erc20.balanceOf(_signatureTransferData.owner);
if (payerBalance < neededAmount) {
revert InsufficientBalance(neededAmount - payerBalance);
}
// Permit this contract to spend the payer's tokens
IERC7597(_intent.recipientCurrency).permit({
owner: _signatureTransferData.owner,
spender: address(this),
value: neededAmount,
deadline: _intent.deadline,
signature: _signatureTransferData.signature
});
// Check the payer has approved this contract for a sufficient transfer
uint256 allowance = erc20.allowance(_signatureTransferData.owner, address(this));
if (allowance < neededAmount) {
revert InsufficientAllowance(neededAmount - allowance);
}
if (neededAmount > 0) {
// Record our balance before (most likely zero) to detect fee-on-transfer tokens
uint256 balanceBefore = erc20.balanceOf(address(this));
// Transfer the payment token to this contract
erc20.safeTransferFrom(_signatureTransferData.owner, address(this), neededAmount);
// Make sure this is not a fee-on-transfer token
revertIfInexactTransfer(neededAmount, balanceBefore, erc20, address(this));
// Complete the payment
transferFundsToDestinations(_intent);
}
succeedPayment(_intent, neededAmount, _intent.recipientCurrency, _signatureTransferData.owner);
}
function swapTokens(
TransferIntent calldata _intent,
address tokenIn,
uint256 maxAmountWillingToPay,
uint24 poolFeesTier
) internal returns (uint256) {
// If the seller is requesting native currency, we need to swap for the wrapped
// version of that currency first, then unwrap it and send it to the seller.
address tokenOut = _intent.recipientCurrency == NATIVE_CURRENCY
? address(wrappedNativeCurrency)
: _intent.recipientCurrency;
// Figure out the total output needed from the swap
uint256 neededAmount = _intent.recipientAmount + _intent.feeAmount;
// Parameters and shared inputs for the universal router
bytes memory uniswap_commands;
bytes[] memory uniswap_inputs;
bytes memory swapPath = abi.encodePacked(tokenOut, poolFeesTier, tokenIn);
bytes memory swapParams = abi.encode(address(uniswap), neededAmount, maxAmountWillingToPay, swapPath, false);
bytes memory transferToRecipient = abi.encode(
_intent.recipientCurrency,
_intent.recipient,
_intent.recipientAmount
);
bytes memory collectFees = abi.encode(
_intent.recipientCurrency,
feeDestinations[_intent.operator],
_intent.feeAmount
);
// The payer's and router's balances before this transaction, used to calculate the amount consumed by the swap
uint256 payerBalanceBefore;
uint256 routerBalanceBefore;
// The fee and recipient balances of the output token, to detect fee-on-transfer tokens
uint256 feeBalanceBefore;
uint256 recipientBalanceBefore;
// Populate the commands and inputs for the universal router
if (msg.value > 0) {
payerBalanceBefore = _msgSender().balance + msg.value;
routerBalanceBefore = address(uniswap).balance + IERC20(wrappedNativeCurrency).balanceOf(address(uniswap));
feeBalanceBefore = IERC20(tokenOut).balanceOf(feeDestinations[_intent.operator]);
recipientBalanceBefore = IERC20(tokenOut).balanceOf(_intent.recipient);
// Paying with ETH, merchant wants tokenOut
uniswap_commands = abi.encodePacked(
bytes1(uint8(UniswapCommands.WRAP_ETH)),
bytes1(uint8(UniswapCommands.V3_SWAP_EXACT_OUT)),
bytes1(uint8(UniswapCommands.TRANSFER)),
bytes1(uint8(UniswapCommands.TRANSFER)),
bytes1(uint8(UniswapCommands.UNWRAP_WETH)), // for the payer refund
bytes1(uint8(UniswapCommands.SWEEP))
);
uniswap_inputs = new bytes[](6);
uniswap_inputs[0] = abi.encode(address(uniswap), msg.value);
uniswap_inputs[1] = swapParams;
uniswap_inputs[2] = collectFees;
uniswap_inputs[3] = transferToRecipient;
uniswap_inputs[4] = abi.encode(address(uniswap), 0);
uniswap_inputs[5] = abi.encode(UniswapConstants.ETH, _msgSender(), 0);
} else {
// No need to check fee/recipient balance of the output token before,
// since we know WETH and ETH are not fee-on-transfer
payerBalanceBefore = IERC20(tokenIn).balanceOf(_msgSender()) + maxAmountWillingToPay;
routerBalanceBefore = IERC20(tokenIn).balanceOf(address(uniswap));
if (_intent.recipientCurrency == NATIVE_CURRENCY) {
// Paying with token, merchant wants ETH
uniswap_commands = abi.encodePacked(
bytes1(uint8(UniswapCommands.V3_SWAP_EXACT_OUT)),
bytes1(uint8(UniswapCommands.UNWRAP_WETH)), // for the recipient
bytes1(uint8(UniswapCommands.TRANSFER)),
bytes1(uint8(UniswapCommands.TRANSFER)),
bytes1(uint8(UniswapCommands.SWEEP))
);
uniswap_inputs = new bytes[](5);
uniswap_inputs[0] = swapParams;
uniswap_inputs[1] = abi.encode(address(uniswap), neededAmount);
uniswap_inputs[2] = collectFees;
uniswap_inputs[3] = transferToRecipient;
uniswap_inputs[4] = abi.encode(tokenIn, _msgSender(), 0);
} else {
feeBalanceBefore = IERC20(tokenOut).balanceOf(feeDestinations[_intent.operator]);
recipientBalanceBefore = IERC20(tokenOut).balanceOf(_intent.recipient);
// Paying with token, merchant wants tokenOut
uniswap_commands = abi.encodePacked(
bytes1(uint8(UniswapCommands.V3_SWAP_EXACT_OUT)),
bytes1(uint8(UniswapCommands.TRANSFER)),
bytes1(uint8(UniswapCommands.TRANSFER)),
bytes1(uint8(UniswapCommands.SWEEP))
);
uniswap_inputs = new bytes[](4);
uniswap_inputs[0] = swapParams;
uniswap_inputs[1] = collectFees;
uniswap_inputs[2] = transferToRecipient;
uniswap_inputs[3] = abi.encode(tokenIn, _msgSender(), 0);
}
// Send the input tokens to Uniswap for the swap
IERC20(tokenIn).safeTransfer(address(uniswap), maxAmountWillingToPay);
}
// Perform the swap
try uniswap.execute{value: msg.value}(uniswap_commands, uniswap_inputs, _intent.deadline) {
// Disallow fee-on-transfer tokens as the output token, since we want to guarantee exact settlement
if (_intent.recipientCurrency != NATIVE_CURRENCY) {
revertIfInexactTransfer(
_intent.feeAmount,
feeBalanceBefore,
IERC20(tokenOut),
feeDestinations[_intent.operator]
);
revertIfInexactTransfer(
_intent.recipientAmount,
recipientBalanceBefore,
IERC20(tokenOut),
_intent.recipient
);
}
// Calculate and return how much of the input token was consumed by the swap. The router
// could have had a balance of the input token prior to this transaction, which would have
// been swept to the payer. This amount, if any, must be accounted for so we don't underflow
// and assume that negative amount of the input token was consumed by the swap.
uint256 payerBalanceAfter;
uint256 routerBalanceAfter;
if (msg.value > 0) {
payerBalanceAfter = _msgSender().balance;
routerBalanceAfter =
address(uniswap).balance +
IERC20(wrappedNativeCurrency).balanceOf(address(uniswap));
} else {
payerBalanceAfter = IERC20(tokenIn).balanceOf(_msgSender());
routerBalanceAfter = IERC20(tokenIn).balanceOf(address(uniswap));
}
return (payerBalanceBefore + routerBalanceBefore) - (payerBalanceAfter + routerBalanceAfter);
} catch Error(string memory reason) {
revert SwapFailedString(reason);
} catch (bytes memory reason) {
bytes32 reasonHash = keccak256(reason);
if (reasonHash == V3_INVALID_SWAP) {
revert SwapFailedString("V3InvalidSwap");
} else if (reasonHash == V3_TOO_LITTLE_RECEIVED) {
revert SwapFailedString("V3TooLittleReceived");
} else if (reasonHash == V3_TOO_MUCH_REQUESTED) {
revert SwapFailedString("V3TooMuchRequested");
} else if (reasonHash == V3_INVALID_AMOUNT_OUT) {
revert SwapFailedString("V3InvalidAmountOut");
} else if (reasonHash == V3_INVALID_CALLER) {
revert SwapFailedString("V3InvalidCaller");
} else {
revert SwapFailedBytes(reason);
}
}
}
function transferFundsToDestinations(TransferIntent calldata _intent) internal {
if (_intent.recipientCurrency == NATIVE_CURRENCY) {
if (_intent.recipientAmount > 0) {
sendNative(_intent.recipient, _intent.recipientAmount, false);
}
if (_intent.feeAmount > 0) {
sendNative(feeDestinations[_intent.operator], _intent.feeAmount, false);
}
} else {
IERC20 requestedCurrency = IERC20(_intent.recipientCurrency);
if (_intent.recipientAmount > 0) {
requestedCurrency.safeTransfer(_intent.recipient, _intent.recipientAmount);
}
if (_intent.feeAmount > 0) {
requestedCurrency.safeTransfer(feeDestinations[_intent.operator], _intent.feeAmount);
}
}
}
function unwrapAndTransferFundsToDestinations(TransferIntent calldata _intent) internal {
uint256 amountToWithdraw = _intent.recipientAmount + _intent.feeAmount;
if (_intent.recipientCurrency == NATIVE_CURRENCY && amountToWithdraw > 0) {
wrappedNativeCurrency.withdraw(amountToWithdraw);
}
transferFundsToDestinations(_intent);
}
function succeedPayment(
TransferIntent calldata _intent,
uint256 spentAmount,
address spentCurrency,
address sender
) internal {
processedTransferIntents[_intent.operator][_intent.id] = true;
emit Transferred(_intent.operator, _intent.id, _intent.recipient, sender, spentAmount, spentCurrency);
}
function sendNative(
address destination,
uint256 amount,
bool isRefund
) internal {
(bool success, bytes memory data) = payable(destination).call{value: amount}("");
if (!success) {
revert NativeTransferFailed(destination, amount, isRefund, data);
}
}
function revertIfInexactTransfer(
uint256 expectedDiff,
uint256 balanceBefore,
IERC20 token,
address target
) internal view {
uint256 balanceAfter = token.balanceOf(target);
if (balanceAfter - balanceBefore != expectedDiff) {
revert InexactTransfer();
}
}
// @notice Registers an operator with a custom fee destination.
function registerOperatorWithFeeDestination(address _feeDestination) external {
feeDestinations[_msgSender()] = _feeDestination;
emit OperatorRegistered(_msgSender(), _feeDestination);
}
// @notice Registers an operator, using the operator's address as the fee destination.
function registerOperator() external {
feeDestinations[_msgSender()] = _msgSender();
emit OperatorRegistered(_msgSender(), _msgSender());
}
function unregisterOperator() external {
delete feeDestinations[_msgSender()];
emit OperatorUnregistered(_msgSender());
}
// @notice Allows the owner to pause the contract.
function pause() external onlyOwner {
_pause();
}
// @notice Allows the owner to un-pause the contract.
function unpause() external onlyOwner {
_unpause();
}
// @dev Required to be able to unwrap WETH
receive() external payable {
require(msg.sender == address(wrappedNativeCurrency), "only payable for unwrapping");
}
}
================================================
FILE: contracts/utils/Sweepable.sol
================================================
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;
import "@openzeppelin/contracts/utils/Context.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
// @title Sweepable contract
// @notice Implements a role that can sweep stuck funds to an address provided
// at the time of the call
abstract contract Sweepable is Context, Ownable {
using SafeERC20 for IERC20;
// @dev The address of the current sweeper
address private _sweeper;
// @dev Restricts the caller to the current sweeper
modifier onlySweeper() {
require(sweeper() == _msgSender(), "Sweepable: not the sweeper");
_;
}
modifier notZero(address a) {
require(a != address(0), "Sweepable: cannot be zero address");
_;
}
// @dev Returns the current sweeper
function sweeper() public view virtual returns (address) {
return _sweeper;
}
// @dev Sets the sweeper
// @notice To remove the sweeper role entirely, set this to the zero address.
function setSweeper(address newSweeper) public virtual onlyOwner notZero(newSweeper) {
_sweeper = newSweeper;
}
// @dev Sweeps the entire ETH balance to `destination`
function sweepETH(address payable destination) public virtual onlySweeper notZero(destination) {
uint256 balance = address(this).balance;
require(balance > 0, "Sweepable: zero balance");
(bool success, ) = destination.call{value: balance}("");
require(success, "Sweepable: transfer error");
}
// @dev Sweeps a specific ETH `amount` to `destination`
function sweepETHAmount(address payable destination, uint256 amount)
public
virtual
onlySweeper
notZero(destination)
{
uint256 balance = address(this).balance;
require(balance >= amount, "Sweepable: insufficient balance");
(bool success, ) = destination.call{value: amount}("");
require(success, "Sweepable: transfer error");
}
// @dev Sweeps the entire token balance to `destination`
function sweepToken(address _token, address destination) public virtual onlySweeper notZero(destination) {
IERC20 token = IERC20(_token);
uint256 balance = token.balanceOf(address(this));
require(balance > 0, "Sweepable: zero balance");
token.safeTransfer(destination, balance);
}
// @dev Sweeps a specific token `amount` to `destination`
function sweepTokenAmount(
address _token,
address destination,
uint256 amount
) public virtual onlySweeper notZero(destination) {
IERC20 token = IERC20(_token);
uint256 balance = token.balanceOf(address(this));
require(balance >= amount, "Sweepable: insufficient balance");
token.safeTransfer(destination, amount);
}
}
gitextract_2euvt6jp/
├── .gitignore
├── LICENSE.md
├── README.md
└── contracts/
├── interfaces/
│ ├── IERC7597.sol
│ ├── ITransfers.sol
│ └── IWrappedNativeCurrency.sol
├── transfers/
│ └── Transfers.sol
└── utils/
└── Sweepable.sol
Condensed preview — 8 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (66K chars).
[
{
"path": ".gitignore",
"chars": 5,
"preview": ".idea"
},
{
"path": "LICENSE.md",
"chars": 10207,
"preview": " Apache License\n Version 2.0, January 2004\n "
},
{
"path": "README.md",
"chars": 6016,
"preview": "# Coinbase Commerce Onchain Payment Protocol\n\nThe Coinbase Commerce Onchain Payment Protocol allows payers and merchants"
},
{
"path": "contracts/interfaces/IERC7597.sol",
"chars": 324,
"preview": "// SPDX-License-Identifier: UNLICENSED\npragma solidity ^0.8.17;\n\nimport \"@openzeppelin/contracts/interfaces/IERC2612.sol"
},
{
"path": "contracts/interfaces/ITransfers.sol",
"chars": 10581,
"preview": "// SPDX-License-Identifier: UNLICENSED\npragma solidity ^0.8.17;\n\nimport \"../permit2/src/interfaces/ISignatureTransfer.so"
},
{
"path": "contracts/interfaces/IWrappedNativeCurrency.sol",
"chars": 306,
"preview": "// SPDX-License-Identifier: UNLICENSED\npragma solidity ^0.8.17;\n\nimport \"@openzeppelin/contracts/token/ERC20/IERC20.sol\""
},
{
"path": "contracts/transfers/Transfers.sol",
"chars": 33938,
"preview": "// SPDX-License-Identifier: UNLICENSED\npragma solidity ^0.8.17;\n\nimport \"@openzeppelin/contracts/token/ERC20/IERC20.sol\""
},
{
"path": "contracts/utils/Sweepable.sol",
"chars": 2958,
"preview": "// SPDX-License-Identifier: UNLICENSED\npragma solidity ^0.8.17;\n\nimport \"@openzeppelin/contracts/utils/Context.sol\";\nimp"
}
]
About this extraction
This page contains the full source code of the coinbase/commerce-onchain-payment-protocol GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 8 files (62.8 KB), approximately 13.8k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.