UsdcPaymentGateway

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

/// @author @mascarock
/// @notice Ethereum-side payment gateway: collects USDC, emits canonical Deposit.
///         Uses Uniswap Permit2 for one-tx UX; falls back to transferFrom if needed.

import {Pausable} from "openzeppelin-contracts/contracts/utils/Pausable.sol";
import {ISignatureTransfer} from "./interfaces/ISignatureTransfer.sol";

// ────────────────────────────────────────────────────────────────────────────────
// Interfaces
interface IERC20 {
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
}

// ────────────────────────────────────────────────────────────────────────────────
contract UsdcPaymentGateway is Pausable {
    address public immutable USDC;
    address public immutable PERMIT2;
    address public VAULT_RECIPIENT_ETH;

    // Fee (optional): protocol fee in basis points taken from amount
    uint96 public feeBps;
    address public owner;

    // Sequential nonce per depositor to bind an intent
    mapping(address => uint256) public depositNonce;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
    event VaultRecipientUpdated(address indexed newVaultRecipient);
    event FeeUpdated(uint96 newFeeBps);

    event Deposit(
        bytes32 indexed depositId,
        address indexed payer,
        uint256 indexed dstChainId,
        address dstRecipient,
        uint256 usdcAmount,
        uint256 netAmount,
        uint256 payerNonce
    );

    modifier onlyOwner() { require(msg.sender == owner, "not owner"); _; }

    constructor(address _usdc, address _permit2, address _vaultRecipient, uint96 _feeBps) {
        require(_usdc != address(0), "invalid USDC");
        require(_permit2 != address(0), "invalid permit2");
        require(_vaultRecipient != address(0), "invalid vault recipient");

        owner = msg.sender;
        USDC = _usdc;
        PERMIT2 = _permit2;
        VAULT_RECIPIENT_ETH = _vaultRecipient;
        require(_feeBps <= 1_000, "fee too high");
        feeBps = _feeBps;

        emit OwnershipTransferred(address(0), msg.sender);
        emit VaultRecipientUpdated(_vaultRecipient);
        emit FeeUpdated(_feeBps);
    }

    function pause() external onlyOwner { _pause(); }
    function unpause() external onlyOwner { _unpause(); }

    function setVaultRecipient(address a) external onlyOwner {
        require(a != address(0), "invalid vault recipient");
        VAULT_RECIPIENT_ETH = a;
        emit VaultRecipientUpdated(a);
    }

    function setFeeBps(uint96 b) external onlyOwner {
        require(b <= 1_000, "fee too high");
        feeBps = b;
        emit FeeUpdated(b);
    }

    function transferOwnership(address n) external onlyOwner {
        require(n != address(0), "invalid owner");
        emit OwnershipTransferred(owner, n);
        owner = n;
    }

    // ────────────────────────────────────────────────────────────────────────────
    // Path A: one-tx UX via Permit2 (owner signs off-chain; no prior approve on USDC)
    function depositWithPermit2(
        ISignatureTransfer.PermitTransferFrom calldata permit,
        bytes calldata sig,
        uint256 dstChainId,
        address dstRecipient
    ) external whenNotPaused returns (bytes32 depositId) {
        uint256 nonce = depositNonce[msg.sender]++;
        uint256 amount = permit.permitted.amount;

        // SECURITY: Validate token is USDC to prevent arbitrary token deposits
        require(permit.permitted.token == USDC, "wrong token");

        // SECURITY: Ensure requested amount matches permitted amount
        require(amount == permit.permitted.amount, "amount mismatch");

        // compute fee; send directly to vault using Permit2
        uint256 fee = (amount * feeBps) / 10_000;
        uint256 net = amount - fee;

        // pull from msg.sender -> VAULT directly
        ISignatureTransfer(PERMIT2).permitTransferFrom(
            permit,
            ISignatureTransfer.SignatureTransferDetails({
                to: VAULT_RECIPIENT_ETH,
                requestedAmount: amount
            }),
            msg.sender,
            sig
        );

        depositId = keccak256(abi.encodePacked(
            block.chainid, address(this), msg.sender, dstChainId, dstRecipient, amount, nonce
        ));

        emit Deposit(depositId, msg.sender, dstChainId, dstRecipient, amount, net, nonce);
    }

    // ────────────────────────────────────────────────────────────────────────────
    // Path B: standard ERC-20 flow if user prefers approve + depositFrom()
    function depositFrom(uint256 amount, uint256 dstChainId, address dstRecipient)
        external
        whenNotPaused
        returns (bytes32 depositId)
    {
        uint256 nonce = depositNonce[msg.sender]++;
        uint256 fee = (amount * feeBps) / 10_000;
        uint256 net = amount - fee;

        require(IERC20(USDC).transferFrom(msg.sender, VAULT_RECIPIENT_ETH, amount), "USDC xfer fail");

        depositId = keccak256(abi.encodePacked(
            block.chainid, address(this), msg.sender, dstChainId, dstRecipient, amount, nonce
        ));

        emit Deposit(depositId, msg.sender, dstChainId, dstRecipient, amount, net, nonce);
    }
}
"
    },
    "lib/openzeppelin-contracts/contracts/utils/Pausable.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.3.0) (utils/Pausable.sol)

pragma solidity ^0.8.20;

import {Context} from "../utils/Context.sol";

/**
 * @dev Contract module which allows children to implement an emergency stop
 * mechanism that can be triggered by an authorized account.
 *
 * This module is used through inheritance. It will make available the
 * modifiers `whenNotPaused` and `whenPaused`, which can be applied to
 * the functions of your contract. Note that they will not be pausable by
 * simply including this module, only once the modifiers are put in place.
 */
abstract contract Pausable is Context {
    bool private _paused;

    /**
     * @dev Emitted when the pause is triggered by `account`.
     */
    event Paused(address account);

    /**
     * @dev Emitted when the pause is lifted by `account`.
     */
    event Unpaused(address account);

    /**
     * @dev The operation failed because the contract is paused.
     */
    error EnforcedPause();

    /**
     * @dev The operation failed because the contract is not paused.
     */
    error ExpectedPause();

    /**
     * @dev Modifier to make a function callable only when the contract is not paused.
     *
     * Requirements:
     *
     * - The contract must not be paused.
     */
    modifier whenNotPaused() {
        _requireNotPaused();
        _;
    }

    /**
     * @dev Modifier to make a function callable only when the contract is paused.
     *
     * Requirements:
     *
     * - The contract must be paused.
     */
    modifier whenPaused() {
        _requirePaused();
        _;
    }

    /**
     * @dev Returns true if the contract is paused, and false otherwise.
     */
    function paused() public view virtual returns (bool) {
        return _paused;
    }

    /**
     * @dev Throws if the contract is paused.
     */
    function _requireNotPaused() internal view virtual {
        if (paused()) {
            revert EnforcedPause();
        }
    }

    /**
     * @dev Throws if the contract is not paused.
     */
    function _requirePaused() internal view virtual {
        if (!paused()) {
            revert ExpectedPause();
        }
    }

    /**
     * @dev Triggers stopped state.
     *
     * Requirements:
     *
     * - The contract must not be paused.
     */
    function _pause() internal virtual whenNotPaused {
        _paused = true;
        emit Paused(_msgSender());
    }

    /**
     * @dev Returns to normal state.
     *
     * Requirements:
     *
     * - The contract must be paused.
     */
    function _unpause() internal virtual whenPaused {
        _paused = false;
        emit Unpaused(_msgSender());
    }
}
"
    },
    "src/interfaces/ISignatureTransfer.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

/// @notice Minimal Permit2 interface for one-time signature-based transfers
interface ISignatureTransfer {
    struct TokenPermissions { address token; uint256 amount; }
    struct PermitTransferFrom {
        TokenPermissions permitted;
        uint256 nonce;
        uint256 deadline;
    }
    struct SignatureTransferDetails { address to; uint256 requestedAmount; }

    function permitTransferFrom(
        PermitTransferFrom calldata permit,
        SignatureTransferDetails calldata transferDetails,
        address owner,
        bytes calldata signature
    ) external;
}"
    },
    "lib/openzeppelin-contracts/contracts/utils/Context.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol)

pragma solidity ^0.8.20;

/**
 * @dev Provides information about the current execution context, including the
 * sender of the transaction and its data. While these are generally available
 * via msg.sender and msg.data, they should not be accessed in such a direct
 * manner, since when dealing with meta-transactions the account sending and
 * paying for execution may not be the actual sender (as far as an application
 * is concerned).
 *
 * This contract is only required for intermediate, library-like contracts.
 */
abstract contract Context {
    function _msgSender() internal view virtual returns (address) {
        return msg.sender;
    }

    function _msgData() internal view virtual returns (bytes calldata) {
        return msg.data;
    }

    function _contextSuffixLength() internal view virtual returns (uint256) {
        return 0;
    }
}
"
    }
  },
  "settings": {
    "remappings": [
      "openzeppelin-contracts/=lib/openzeppelin-contracts/",
      "forge-std/=lib/forge-std/src/",
      "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/",
      "erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/",
      "halmos-cheatcodes/=lib/openzeppelin-contracts/lib/halmos-cheatcodes/src/"
    ],
    "optimizer": {
      "enabled": true,
      "runs": 200
    },
    "metadata": {
      "useLiteralContent": false,
      "bytecodeHash": "ipfs",
      "appendCBOR": true
    },
    "outputSelection": {
      "*": {
        "*": [
          "evm.bytecode",
          "evm.deployedBytecode",
          "devdoc",
          "userdoc",
          "metadata",
          "abi"
        ]
      }
    },
    "evmVersion": "paris",
    "viaIR": true
  }
}}

Tags:
Multisig, Multi-Signature, Factory|addr:0x4758ba3b7d0631729b73e907b4d07fb96996bfd1|verified:true|block:23670557|tx:0x2d7d617364f8d41a4bb75ba75ef8707b72ca7cf91393f2f9050eae4515ae1cd4|first_check:1761589590

Submitted on: 2025-10-27 19:26:31

Comments

Log in to comment.

No comments yet.