A Solidity smart contract for managing payments with security features and ownership control, developed using Hardhat.
The PaymentManager
smart contract is designed to manage payments in various cryptocurrencies. It includes features to:
- Accept payments in specified cryptocurrencies.
- Emit events for each payment for server-side verification.
- Allow the owner to change the payment address and accepted cryptocurrencies.
- Enable pausing and unpausing of the payment function.
- Withdraw stuck funds by the owner.
The PaymentManager
contract has been already deployed on the Holesky network to the following address:
0xaDD20dfD083bDD5D08FE83a05553ad22687549F4
Main preqrequisities are as listed. For all dependencies and version information please refer to package.json
- Node.js and npm: Download and install Node.js
- Infura account: Sign up for Infura and create a new project to get your Infura project ID.
- Etherscan account: Sign up for Etherscan and obtain an API key.
- Solidity ^0.8.20
- OpenZeppelin Contracts
-
Clone the repository:
git clone <repository_url> cd payment-manager
-
Install dependencies:
npm install
-
Create a
.env
file in the root directory and add your Infura project ID, mnemonic, and Etherscan API key:MNEMONIC="your twelve word mnemonic" INFURA_PROJECT_ID="your infura project id" ETHERSCAN_API_KEY="your etherscan api key"
contracts/
: Contains the Solidity smart contract.scripts/
: Contains the deployment script.test/
: Contains the test scripts.hardhat.config.js
: Hardhat configuration file.package.json
: Project dependencies and scripts.
To compile the Solidity contracts using Hardhat:
npm run compile
To run the tests using Hardhat:
npm run test
To deploy the contract to the local Hardhat network:
npm run deploy
To deploy the contract to the Sepolia test network:
npm run deploy:sepolia
To deploy the contract to the Holesky test network:
npm run deploy:holesky
To deploy the contract to the Ethereum mainnet:
npm run deploy:mainnet
To verify the contract on Etherscan for the Sepolia test network:
npm run verify:ropsten <deployed_contract_address> <constructor_arguments>
To verify the contract on Etherscan for the Holesky test network:
npm run verify:rinkeby <deployed_contract_address> <constructor_arguments>
To verify the contract on Etherscan for the Ethereum mainnet:
npm run verify:mainnet <deployed_contract_address> <constructor_arguments>
Please refer to (scripts/deploy.js
):
Please refer to (test/PaymentManager.test.mjs
):
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
contract PaymentManager is ReentrancyGuard, Ownable, Pausable {
// Contract code
}
address public paymentAddress
: Address to receive payments.mapping(address => bool) public acceptedCryptocurrencies
: Mapping of accepted cryptocurrencies.
event PaymentReceived(address indexed donor, address indexed cryptocurrency, uint256 amount, string uniqueIdentifier)
: Emitted when a payment is received.event CryptocurrencyStatusChanged(address indexed cryptocurrency, bool status)
: Emitted when the status of a cryptocurrency is changed.
Initializes the contract with the payment address and accepted cryptocurrencies.
constructor(address _paymentAddress, address[] memory _acceptedCryptocurrencies) {
require(_paymentAddress != address(0), "Payment address cannot be zero address");
paymentAddress = _paymentAddress;
for (uint i = 0; i < _acceptedCryptocurrencies.length; i++) {
acceptedCryptocurrencies[_acceptedCryptocurrencies[i]] = true;
emit CryptocurrencyStatusChanged(_acceptedCryptocurrencies[i], true);
}
}
Changes the payment address. Only the owner can call this function.
function changePaymentAddress(address _newAddress) external onlyOwner {
require(_newAddress != address(0), "New address cannot be zero address");
paymentAddress = _newAddress;
}
Toggles the acceptance status of a cryptocurrency. Only the owner can call this function.
function toggleAcceptedCryptocurrency(address _cryptocurrency, bool _status) external onlyOwner {
acceptedCryptocurrencies[_cryptocurrency] = _status;
emit CryptocurrencyStatusChanged(_cryptocurrency, _status);
}
Allows users to pay using specified cryptocurrencies. Emits a PaymentReceived
event.
function pay(address _cryptocurrency, string memory uniqueIdentifier) external payable whenNotPaused nonReentrant {
require(acceptedCryptocurrencies[_cryptocurrency], "Cryptocurrency not accepted");
require(msg.value > 0, "Payment must be greater than zero");
emit PaymentReceived(msg.sender, _cryptocurrency, msg.value, uniqueIdentifier);
(bool success, ) = paymentAddress.call{value: msg.value}("");
require(success, "Transfer failed");
}
Pauses the payment function. Only the owner can call this function.
function pause() external onlyOwner {
_pause();
}
Unpauses the payment function. Only the owner can call this function.
function unpause() external onlyOwner {
_unpause();
}
Allows the owner to withdraw stuck funds from the contract. Only the owner can call this function.
function withdraw() external onlyOwner nonReentrant {
uint256 balance = address(this).balance;
require(balance > 0, "No funds to withdraw");
(bool success, ) = owner().call{value: balance}("");
require(success, "Withdraw failed");
}
Receives ETH sent directly to the contract.
receive() external payable {}
Deploy the contract with the initial payment address and accepted cryptocurrencies.
constructor(address _paymentAddress, address[] memory _acceptedCryptocurrencies)
Only the owner can change the payment address.
function changePaymentAddress(address _newAddress) external onlyOwner
Only the owner can toggle the acceptance status of a cryptocurrency.
function toggleAcceptedCryptocurrency(address _cryptocurrency, bool _status) external onlyOwner
Users can pay using the specified cryptocurrencies.
function pay(address _cryptocurrency, string memory uniqueIdentifier) external payable whenNotPaused nonReentrant
Only the owner can pause and unpause the payment function.
function pause() external onlyOwner
function unpause() external onlyOwner
Only the owner can withdraw stuck funds from the contract.
function withdraw() external onlyOwner nonReentrant
Listen for PaymentReceived
events to verify transactions server-side.
The PaymentManager
smart contract incorporates several security features to ensure the safety and integrity of the payment process. Below is a detailed explanation of the security measures implemented:
The contract uses OpenZeppelin's ReentrancyGuard
to protect against reentrancy attacks. Reentrancy attacks occur when an external contract makes a recursive call back into the target contract before the initial function execution is complete, potentially exploiting the contract's state.
Implementation:
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract PaymentManager is ReentrancyGuard {
// Functions using the nonReentrant modifier
function pay(address _cryptocurrency, string memory uniqueIdentifier) external payable whenNotPaused nonReentrant {
// Function logic
}
function withdraw() external onlyOwner nonReentrant {
// Function logic
}
}
The contract uses OpenZeppelin's Ownable
to restrict access to sensitive functions to the contract owner. This ensures that only authorized personnel can perform critical operations such as changing the payment address or toggling accepted cryptocurrencies.
Implementation:
import "@openzeppelin/contracts/access/Ownable.sol";
contract PaymentManager is Ownable {
function changePaymentAddress(address _newAddress) external onlyOwner {
// Function logic
}
function toggleAcceptedCryptocurrency(address _cryptocurrency, bool _status) external onlyOwner {
// Function logic
}
}
The contract uses OpenZeppelin's Pausable
to provide the ability to pause and unpause the payment function. This feature is useful in emergency situations, allowing the owner to temporarily halt payments to address any issues or vulnerabilities.
Implementation:
import "@openzeppelin/contracts/security/Pausable.sol";
contract PaymentManager is Pausable {
function pause() external onlyOwner {
_pause();
}
function unpause() external onlyOwner {
_unpause();
}
}
The pay
function follows the checks-effects-interactions pattern to prevent reentrancy attacks. By emitting the event before making an external call, the contract ensures that state changes are made before interacting with external addresses.
Implementation:
function pay(address _cryptocurrency, string memory uniqueIdentifier) external payable whenNotPaused nonReentrant {
require(acceptedCryptocurrencies[_cryptocurrency], "Cryptocurrency not accepted");
require(msg.value > 0, "Payment must be greater than zero");
emit PaymentReceived(msg.sender, _cryptocurrency, msg.value, uniqueIdentifier);
(bool success, ) = paymentAddress.call{value: msg.value}("");
require(success, "Transfer failed");
}
The contract includes a withdraw
function that allows the owner to recover any funds that might get stuck in the contract. This function ensures that the contract's balance can be safely transferred to the owner if needed.
Implementation:
function withdraw() external onlyOwner nonReentrant {
uint256 balance = address(this).balance;
require(balance > 0, "No funds to withdraw");
(bool success, ) = owner().call{value: balance}("");
require(success, "Withdraw failed");
}
By implementing these security features, the PaymentManager
contract ensures a high level of protection against common vulnerabilities and attacks, providing a secure platform for managing cryptocurrency payments.
MIT License