Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions contracts/InvoicePayments.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

/**
* @title InvoicePayments
* @notice Transfers accepted ERC20 invoice payments from payer to payee and records payment metadata
*/
contract InvoicePayments is Ownable, ReentrancyGuard {
using SafeERC20 for IERC20;

mapping(address => bool) public acceptedTokens;

event InvoicePaid(
address indexed tokenAddress,
uint256 tokenAmount,
address indexed payerAddress,
address indexed payeeAddress,
string memo
);

event AcceptedTokenUpdated(address indexed tokenAddress, bool accepted);

error TokenNotAccepted(address tokenAddress);
error InvalidAddress();
error InvalidAmount();
error EmptyMemo();

constructor(address initialOwner, address[] memory initialAcceptedTokens) Ownable(initialOwner) {
if (initialOwner == address(0)) {
revert InvalidAddress();
}

uint256 len = initialAcceptedTokens.length;
for (uint256 i = 0; i < len; i++) {
address token = initialAcceptedTokens[i];
if (token == address(0)) {
revert InvalidAddress();
}
acceptedTokens[token] = true;
emit AcceptedTokenUpdated(token, true);
}
}

function setAcceptedToken(address tokenAddress, bool accepted) external onlyOwner {
if (tokenAddress == address(0)) {
revert InvalidAddress();
}

acceptedTokens[tokenAddress] = accepted;
emit AcceptedTokenUpdated(tokenAddress, accepted);
}

/**
* @notice Pays an invoice by transferring tokens from payer to payee
* @dev memo may follow this convention: "<contract_code>:<installmentId>"
*/
function payInvoice(
address tokenAddress,
uint256 tokenAmount,
address payerAddress,
address payeeAddress,
string calldata memo
) external nonReentrant {
if (!acceptedTokens[tokenAddress]) {
revert TokenNotAccepted(tokenAddress);
}
if (payerAddress == address(0) || payeeAddress == address(0)) {
revert InvalidAddress();
}
if (tokenAmount == 0) {
revert InvalidAmount();
}
if (bytes(memo).length == 0) {
revert EmptyMemo();
}

IERC20(tokenAddress).safeTransferFrom(payerAddress, payeeAddress, tokenAmount);

emit InvoicePaid(tokenAddress, tokenAmount, payerAddress, payeeAddress, memo);
}
}
66 changes: 66 additions & 0 deletions contracts/UniversalDexRouter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ contract UniversalDexRouter is AccessControl, ReentrancyGuard {
// Roles
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");

// Tokens accepted for direct invoice payments
mapping(address => bool) public acceptedTokens;

// DEX Router address (Uniswap or PancakeSwap)
address public immutable router;

Expand Down Expand Up @@ -153,6 +156,19 @@ contract UniversalDexRouter is AccessControl, ReentrancyGuard {
uint256 fee
);
event FeeReceiverUpdated(address indexed oldReceiver, address indexed newReceiver);
event InvoicePaid(
address indexed tokenAddress,
uint256 tokenAmount,
address indexed payerAddress,
address indexed payeeAddress,
string memo
);
event AcceptedTokenUpdated(address indexed tokenAddress, bool accepted);

error TokenNotAccepted(address tokenAddress);
error InvalidAddress();
error InvalidAmount();
error EmptyMemo();

/**
* @dev Constructor
Expand Down Expand Up @@ -195,6 +211,56 @@ contract UniversalDexRouter is AccessControl, ReentrancyGuard {
emit FeeReceiverUpdated(oldReceiver, newFeeReceiver);
}

/**
* @dev Sets whether a token can be used for direct invoice payment.
* @param tokenAddress Token address to configure
* @param accepted Whether the token is accepted
*/
function setAcceptedToken(address tokenAddress, bool accepted) external onlyRole(ADMIN_ROLE) {
if (tokenAddress == address(0)) {
revert InvalidAddress();
}

acceptedTokens[tokenAddress] = accepted;
emit AcceptedTokenUpdated(tokenAddress, accepted);
}

/**
* @notice Pays an invoice by transferring accepted ERC20 tokens from payer to payee.
* @dev memo may follow this convention: "<contract_code>:<installmentId>"
*/
function payInvoice(
address tokenAddress,
uint256 tokenAmount,
address payeeAddress,
string calldata memo
) external nonReentrant {
if (!acceptedTokens[tokenAddress]) {
revert TokenNotAccepted(tokenAddress);
}

if (tokenAmount == 0) {
revert InvalidAmount();
}
if (bytes(memo).length == 0) {
revert EmptyMemo();
}
IERC20(tokenAddress).safeTransferFrom(msg.sender, address(this), tokenAmount);

IERC20(tokenAddress).safeTransfer(payeeAddress, tokenAmount);

emit SwapExecuted(
msg.sender,
payeeAddress,
tokenAddress,
tokenAddress,
tokenAmount,
tokenAmount,
tokenAmount,
memo
);
}

/**
* @dev Swap exact tokens for tokens on Uniswap V2 / PancakeSwap
* @param params SwapV2Params struct containing all swap parameters
Expand Down
Loading