DevStation / LaunchKit / Templates / Stablecoin Invoices

Stablecoin Invoices

Issue invoices payable in a stablecoin and collect them onchain, with a paid/cancelled record per invoice. Pre-filled with QUSDC on QIE Mainnet.

Deploy This Template
StablecoinInvoices.sol
solidity
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.20;
3 
4interface IERC20 {
5 function transferFrom(address from, address to, uint256 amount) external returns (bool);
6 function transfer(address to, uint256 amount) external returns (bool);
7 function balanceOf(address account) external view returns (uint256);
8 function decimals() external view returns (uint8);
9}
10 
11/// @title StablecoinInvoices
12/// @notice Issue invoices payable in a stablecoin (QUSDC on QIE) and collect
13/// them onchain, with a per-invoice paid/cancelled record.
14/// @dev Amounts are in the TOKEN'S OWN smallest unit. QUSDC has 6 decimals,
15/// not 18: 1 QUSDC is 1_000_000. Never assume 1e18 here.
16/// Uses transferFrom, so a payer must approve() this contract for the
17/// invoice amount first. Pull-based on withdraw, and state is written
18/// before the external call on every path.
19contract StablecoinInvoices {
20 struct Invoice {
21 address payer; // address(0) = payable by anyone
22 uint256 amount; // in token smallest units
23 uint64 dueBy; // unix seconds; 0 = no expiry
24 bool paid;
25 bool cancelled;
26 string memo;
27 }
28 
29 IERC20 public immutable token;
30 address public owner;
31 uint256 public invoiceCount;
32 uint256 public totalCollected;
33 mapping(uint256 => Invoice) private _invoices;
34 
35 event InvoiceIssued(uint256 indexed id, address indexed payer, uint256 amount, string memo);
36 event InvoicePaid(uint256 indexed id, address indexed paidBy, uint256 amount);
37 event InvoiceCancelled(uint256 indexed id);
38 event Withdrawn(address indexed to, uint256 amount);
39 event OwnerChanged(address indexed previousOwner, address indexed newOwner);
40 
41 error NotOwner();
42 error ZeroAddress();
43 error NotAContract();
44 error ZeroAmount();
45 error NoSuchInvoice();
46 error AlreadySettled();
47 error NotYourInvoice();
48 error PastDue();
49 error TransferFailed();
50 error NothingToWithdraw();
51 
52 modifier onlyOwner() {
53 if (msg.sender != owner) revert NotOwner();
54 _;
55 }
56 
57 /// @param token_ Stablecoin accepted for payment (QUSDC on QIE Mainnet).
58 /// @param initialOwner Receives collected funds and may issue invoices.
59 constructor(address token_, address initialOwner) {
60 if (token_ == address(0) || initialOwner == address(0)) revert ZeroAddress();
61 // token is immutable: a wrong address means no invoice can ever be
62 // paid and nothing can be withdrawn, with no way to correct it.
63 if (token_.code.length == 0) revert NotAContract();
64 token = IERC20(token_);
65 owner = initialOwner;
66 emit OwnerChanged(address(0), initialOwner);
67 }
68 
69 /// @notice Issue an invoice. `payer` may be address(0) for "anyone can pay".
70 /// @param amount In the token's smallest unit (QUSDC: 1 QUSDC = 1000000).
71 /// @param dueBy Unix seconds after which payment is refused; 0 = no expiry.
72 function issueInvoice(address payer, uint256 amount, uint64 dueBy, string calldata memo)
73 external
74 onlyOwner
75 returns (uint256 id)
76 {
77 if (amount == 0) revert ZeroAmount();
78 id = ++invoiceCount;
79 _invoices[id] = Invoice(payer, amount, dueBy, false, false, memo);
80 emit InvoiceIssued(id, payer, amount, memo);
81 }
82 
83 /// @notice Pay an invoice. Approve this contract for `amount` first.
84 function payInvoice(uint256 id) external {
85 Invoice storage inv = _invoices[id];
86 if (inv.amount == 0) revert NoSuchInvoice();
87 if (inv.paid || inv.cancelled) revert AlreadySettled();
88 if (inv.payer != address(0) && inv.payer != msg.sender) revert NotYourInvoice();
89 if (inv.dueBy != 0 && block.timestamp > inv.dueBy) revert PastDue();
90 
91 inv.paid = true; // effects before interaction
92 totalCollected += inv.amount;
93 emit InvoicePaid(id, msg.sender, inv.amount);
94 
95 if (!token.transferFrom(msg.sender, address(this), inv.amount)) revert TransferFailed();
96 }
97 
98 function cancelInvoice(uint256 id) external onlyOwner {
99 Invoice storage inv = _invoices[id];
100 if (inv.amount == 0) revert NoSuchInvoice();
101 if (inv.paid || inv.cancelled) revert AlreadySettled();
102 inv.cancelled = true;
103 emit InvoiceCancelled(id);
104 }
105 
106 /// @notice Send the contract's entire token balance to `to`.
107 function withdraw(address to) external onlyOwner {
108 if (to == address(0)) revert ZeroAddress();
109 uint256 balance = token.balanceOf(address(this));
110 if (balance == 0) revert NothingToWithdraw();
111 emit Withdrawn(to, balance);
112 if (!token.transfer(to, balance)) revert TransferFailed();
113 }
114 
115 function transferOwnership(address newOwner) external onlyOwner {
116 if (newOwner == address(0)) revert ZeroAddress();
117 emit OwnerChanged(owner, newOwner);
118 owner = newOwner;
119 }
120 
121 function getInvoice(uint256 id) external view returns (Invoice memory) {
122 if (_invoices[id].amount == 0) revert NoSuchInvoice();
123 return _invoices[id];
124 }
125 
126 function isPayable(uint256 id) external view returns (bool) {
127 Invoice storage inv = _invoices[id];
128 return inv.amount != 0 && !inv.paid && !inv.cancelled
129 && (inv.dueBy == 0 || block.timestamp <= inv.dueBy);
130 }
131}
132 
DevStation
Loading console…