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
}
}}
Submitted on: 2025-10-27 19:26:31
Comments
Log in to comment.
No comments yet.