PayNode is a sophisticated non-custodial payment aggregation protocol built on the Ethereum Virtual Machine (EVM). It facilitates intelligent, parallel settlement routing for off-chain liquidity providers, ensuring efficient and secure transactions. This project leverages robust Solidity smart contracts, OpenZeppelin standards for security and upgradeability, and integrates Chainlink Automation for decentralized governance and autonomous operations.
- Modular Architecture: Designed with distinct layers for Access Control, Settings, and Core Gateway logic, enhancing maintainability and security.
- Non-Custodial Escrow: Funds are securely held in smart contract escrow until settlement, never directly by the protocol or providers.
- Tier-Based Intelligent Routing: Classifies orders into dynamic tiers (ALPHA, BETA, DELTA, OMEGA, TITAN) to optimize provider matching and settlement speed.
- Parallel Settlement Proposals: Enables multiple liquidity providers to simultaneously bid on settlement orders, promoting competition and faster execution.
- UUPS Upgradeability: Implements a secure Universal Upgradeable Proxy Standard (UUPS) pattern, allowing for future contract enhancements without migrating state.
- Role-Based Access Control (RBAC): Granular permissions managed by
PayNodeAccessManagerensures operations are restricted to authorized roles (Admin, Aggregator, Provider, Operator, Dispute Manager, Platform Service, Fee Manager). - Timelocked Governance: Critical administrative changes and contract upgrades are subject to a 48-hour timelock via
PayNodeAdmin, preventing hasty or malicious modifications. - Provider Reputation System: Tracks provider performance (successful orders, settlement time, no-shows) to inform routing decisions and maintain service quality.
- Integrator Self-Service: Allows dApps and partners to register, set their own fees, and manage their integration directly.
- Emergency Pause/Shutdown: Provides a failsafe mechanism to halt critical contract operations during emergencies.
- Replay Protection: Utilizes user nonces and message hashes to secure off-chain signed actions and prevent double-spending.
- Chainlink Automation Integration: Automates the execution of timelocked upgrades and other routine maintenance tasks, ensuring reliability and decentralization.
To get a local copy up and running, follow these steps.
- Clone the Repository:
git clone https://github.com/olujimiAdebakin/paynode-contract.git cd paynode-contract - Initialize Submodules:
This project uses Git submodules for external dependencies (OpenZeppelin, Foundry, Chainlink).
git submodule update --init --recursive
- Install Foundry:
If you don't have Foundry installed, follow the instructions here.
curl -L https://foundry.paradigm.xyz | bash foundryup - Build Contracts:
Compile the Solidity smart contracts using Foundry.
forge build
Before deployment or interaction, you will need to set up the following environment variables. Create a .env file in the project root and populate it with your specific values.
RPC_URL: Your Ethereum Virtual Machine (EVM) compatible blockchain node URL (e.g., Alchemy, Infura, local Anvil instance).- Example:
RPC_URL=https://eth-sepolia.g.alchemy.com/v2/YOUR_ALCHEMY_KEY
- Example:
PRIVATE_KEY: The private key of the account used for deployment and transactions. Handle with extreme care.- Example:
PRIVATE_KEY=0x...
- Example:
ETHERSCAN_API_KEY: (Optional) API key for block explorers (e.g., Etherscan, Polygonscan) for contract verification.- Example:
ETHERSCAN_API_KEY=YOUR_ETHERSCAN_API_KEY
- Example:
DEPLOYER_ADDRESS: The public address corresponding to yourPRIVATE_KEY.- Example:
DEPLOYER_ADDRESS=0xYourDeployerAddress
- Example:
SUPER_ADMIN_ADDRESS: The address designated as the initialDEFAULT_ADMIN_ROLEholder.- Example:
SUPER_ADMIN_ADDRESS=0xYourSuperAdminAddress
- Example:
AGGREGATOR_ADDRESS: The address of the off-chain aggregator service (or a designated wallet).- Example:
AGGREGATOR_ADDRESS=0xYourAggregatorServiceAddress
- Example:
TREASURY_ADDRESS: The address where protocol fees will be collected.- Example:
TREASURY_ADDRESS=0xYourTreasuryWalletAddress
- Example:
CHAINLINK_KEEPER_ADDRESS: The address of the authorized Chainlink Keeper for automated tasks.- Example:
CHAINLINK_KEEPER_ADDRESS=0xYourChainlinkKeeperAddress
- Example:
PROTOCOL_FEE_BPS: Initial protocol fee in basis points (e.g.,100for 1%).- Example:
PROTOCOL_FEE_BPS=100
- Example:
ORDER_EXPIRY_WINDOW: Default duration for order expiration in seconds.- Example:
ORDER_EXPIRY_WINDOW=3600(1 hour)
- Example:
PROPOSAL_TIMEOUT: Default timeout for settlement proposals in seconds.- Example:
PROPOSAL_TIMEOUT=300(5 minutes)
- Example:
INTENT_EXPIRY: Default expiration time for provider intents in seconds.- Example:
INTENT_EXPIRY=600(10 minutes)
- Example:
ALPHA_TIER_LIMIT,BETA_TIER_LIMIT,DELTA_TIER_LIMIT,OMEGA_TIER_LIMIT,TITAN_TIER_LIMIT: Tier limits for order classification (in smallest unit of the token, e.g., wei for ETH).- Example:
ALPHA_TIER_LIMIT=3000000000000000000000(3000 tokens)
- Example:
INTEGRATOR_ADDRESS: The default integrator address for initial setup.- Example:
INTEGRATOR_ADDRESS=0xYourIntegratorAddress
- Example:
INTEGRATOR_FEE_BPS: The default integrator fee in basis points.- Example:
INTEGRATOR_FEE_BPS=50(0.5%)
- Example:
This section details the public interfaces and functions of the core PayNode smart contracts. All interactions are via blockchain transactions or view calls.
Overview: The central contract for managing roles, blacklisting, and system-wide state flags. It implements UUPS upgradeability and integrates Pausable and ReentrancyGuard functionalities.
Contract Address: [Deployed PayNodeAccessManager Address]
Description: Returns the bytes32 identifier for the ADMIN_ROLE.
Request: None
Response: bytes32 - The role identifier.
Errors: None
Description: Returns the bytes32 identifier for the OPERATOR_ROLE.
Request: None
Response: bytes32 - The role identifier.
Errors: None
Description: Returns the bytes32 identifier for the DISPUTE_MANAGER_ROLE.
Request: None
Response: bytes32 - The role identifier.
Errors: None
Description: Returns the bytes32 identifier for the PLATFORM_SERVICE_ROLE.
Request: None
Response: bytes32 - The role identifier.
Errors: None
Description: Returns the bytes32 identifier for the DEFAULT_ADMIN_ROLE.
Request: None
Response: bytes32 - The role identifier.
Errors: None
Description: Returns the bytes32 identifier for the AGGREGATOR_ROLE.
Request: None
Response: bytes32 - The role identifier.
Errors: None
Description: Returns the bytes32 identifier for the FEE_MANAGER_ROLE.
Request: None
Response: bytes32 - The role identifier.
Errors: None
Description: Returns the bytes32 identifier for the TRADING_ENABLED system flag.
Request: None
Response: bytes32 - The flag identifier.
Errors: None
Description: Returns the bytes32 identifier for the WITHDRAWALS_ENABLED system flag.
Request: None
Response: bytes32 - The flag identifier.
Errors: None
Description: Checks if the system is in an emergency locked state.
Request: None
Response: bool - True if locked, false otherwise.
Errors: None
Description: Returns the address of the PasarAdmin contract (holder of ADMIN_ROLE).
Request: None
Response: address - The PasarAdmin contract address.
Errors: None
Description: Returns the address of the superAdmin (holder of DEFAULT_ADMIN_ROLE).
Request: None
Response: address - The superAdmin address.
Errors: None
Description: Returns the minimum delay (in seconds) for timelocked operations.
Request: None
Response: uint256 - Minimum delay in seconds.
Errors: None
Description: Retrieves the status of a specific system flag. Request:
bytes32 flag // E.g., keccak256("TRADING_ENABLED")
Response: bool - True if the flag is enabled, false otherwise.
Errors: None
Description: Checks if a given address is blacklisted. Request:
address user // Address to check
Response: bool - True if blacklisted, false otherwise.
Errors: None
Function Type: Transaction initialize(address _pasarAdmin, address _superAdmin, address[] calldata operators)
Description: Initializes the PayNodeAccessManager contract, setting up initial roles and administrators. This function should only be called once, immediately after proxy deployment.
Request:
address _pasarAdmin // Address of the PasarAdmin contract (for ADMIN_ROLE)
address _superAdmin // Address for the DEFAULT_ADMIN_ROLE
address[] operators // Array of addresses to receive OPERATOR_ROLE
Response: No direct return, emits RoleAssigned events.
Errors:
InvalidAddress: If_superAdmin,_pasarAdmin, or anyoperatorisaddress(0).InvalidRoleConfiguration: If anyoperatoris already a super admin or Pasar admin.
Description: Updates the status of a predefined system flag (e.g., TRADING_ENABLED, WITHDRAWALS_ENABLED).
Request:
bytes32 flag // Identifier of the system flag
bool status // New status (true for enabled, false for disabled)
Response: No direct return, emits SystemFlagUpdated.
Errors:
AccessControl: sender missing role(DEFAULT_ADMIN_ROLE)Pausable: paused: If the contract is paused.UnauthorizedOperation: If the system is locked (systemLocked == true).SystemFlagNotFound: Ifflagis notTRADING_ENABLEDorWITHDRAWALS_ENABLED.
Description: Updates the blacklist status for a single user address. Request:
address user // Address to update
bool status // New blacklist status (true to blacklist, false to unblacklist)
Response: No direct return, emits BlacklistStatusChanged.
Errors:
AccessControl: sender missing role(OPERATOR_ROLE)Pausable: paused: If the contract is paused.UnauthorizedOperation: If the system is locked (systemLocked == true).InvalidAddress: Ifuserisaddress(0).UnauthorizedOperation: If attempting to blacklist anADMIN_ROLEorDEFAULT_ADMIN_ROLEholder.
Description: Updates the blacklist status for multiple user addresses in a single transaction. Request:
address[] users // Array of addresses to update
bool[] statuses // Array of boolean statuses (true/false)
Response: No direct return, emits BlacklistStatusChanged for each user.
Errors:
AccessControl: sender missing role(OPERATOR_ROLE)Pausable: paused: If the contract is paused.UnauthorizedOperation: If the system is locked (systemLocked == true).InvalidRoleConfiguration: Ifusers.length != statuses.length.InvalidAddress: If anyuserisaddress(0).UnauthorizedOperation: If attempting to blacklist anADMIN_ROLEorDEFAULT_ADMIN_ROLEholder.
Description: Schedules a timelocked change for either the superAdmin or pasarAdmin address.
Request:
address newAdmin // Proposed new admin address
bool isSuperAdmin // True for DEFAULT_ADMIN_ROLE, false for ADMIN_ROLE
Response: No direct return, emits AdminChangeScheduled.
Errors:
AccessControl: sender missing role(DEFAULT_ADMIN_ROLE)Pausable: paused: If the contract is paused.UnauthorizedOperation: If the system is locked (systemLocked == true).InvalidAddress: IfnewAdminisaddress(0).
Description: Executes a previously scheduled admin change after its timelock has passed. Request:
bytes32 operationId // Unique identifier of the pending admin change
Response: No direct return, emits RoleAssigned.
Errors:
AccessControl: sender missing role(DEFAULT_ADMIN_ROLE)Pausable: paused: If the contract is paused.UnauthorizedOperation: If the system is locked (systemLocked == true).UnauthorizedOperation: IfoperationIdis invalid or timelock has not passed.
Description: Cancels a pending admin change operation. Request:
bytes32 operationId // Unique identifier of the pending admin change to cancel
Response: No direct return, emits AdminChangeScheduled with scheduleTime = 0.
Errors:
AccessControl: sender missing role(DEFAULT_ADMIN_ROLE)Pausable: paused: If the contract is paused.UnauthorizedOperation: If the system is locked (systemLocked == true).UnauthorizedOperation: IfoperationIdis invalid or not found.
Description: Initiates an emergency shutdown, locking the system and pausing the contract.
Request: None
Response: No direct return, emits EmergencyShutdown.
Errors:
AccessControl: sender missing role(DEFAULT_ADMIN_ROLE)
Description: Restores core system functions after an emergency shutdown, unlocking the system and unpausing the contract.
Request: None
Response: No direct return, emits SystemRestored.
Errors:
AccessControl: sender missing role(DEFAULT_ADMIN_ROLE)
Description: Pauses the contract, preventing calls to functions protected by whenNotPaused.
Request: None
Response: No direct return, emits Paused.
Errors:
AccessControl: sender missing role(DEFAULT_ADMIN_ROLE)UnauthorizedOperation: If the system is locked (systemLocked == true).
Description: Unpauses the contract, allowing calls to functions protected by whenNotPaused again.
Request: None
Response: No direct return, emits Unpaused.
Errors:
AccessControl: sender missing role(DEFAULT_ADMIN_ROLE)UnauthorizedOperation: If the system is locked (systemLocked == true).
Description: (Virtual) Resolves a dispute. Intended to be overridden by a derived contract. Request:
uint256 disputeId // Unique ID of the dispute
address winner // Address of the party who won the dispute
Response: No direct return. Errors:
AccessControl: sender missing role(DISPUTE_MANAGER_ROLE)Pausable: paused: If the contract is paused.UnauthorizedOperation: If the system is locked (systemLocked == true).InvalidAddress: Ifwinnerisaddress(0).
Description: (Virtual) Manages platform services. Intended to be overridden by a derived contract. Request:
bytes32 serviceId // Unique identifier of the service
bool enable // True to enable, false to disable
Response: No direct return. Errors:
AccessControl: sender missing role(PLATFORM_SERVICE_ROLE)Pausable: paused: If the contract is paused.UnauthorizedOperation: If the system is locked (systemLocked == true).
Description: Checks if an address holds the OPERATOR_ROLE.
Request:
address account // Address to check
Response: bool - True if account is an operator, false otherwise.
Errors: None
Description: Retrieves all specific roles assigned to a given address. Request:
address account // Address to check
Response: bytes32[] - An array of role identifiers held by the account.
Errors: None
Description: Checks if a scheduled admin change operation is ready for execution. Request:
bytes32 operationId // Unique identifier of the pending admin change
Response: bool - True if the operation exists and its timelock has passed.
Errors: None
Overview: The configuration hub for the PayNode protocol, storing and managing all configurable parameters like fees, tier limits, and crucial addresses. It implements OwnableUpgradeable for administrative control.
Contract Address: [Deployed PGatewaySettings Address]
Description: Returns the constant representing 100% in basis points (100,000).
Request: None
Response: uint256 - The value 100_000.
Errors: None
Description: Returns the current protocol fee percentage in basis points.
Request: None
Response: uint64 - Protocol fee.
Errors: None
Description: Returns the configured order expiry window duration in seconds.
Request: None
Response: uint256 - Order expiry window.
Errors: None
Description: Returns the proposal timeout duration in seconds.
Request: None
Response: uint256 - Proposal timeout.
Errors: None
Description: Returns the current treasury address.
Request: None
Response: address - Treasury address.
Errors: None
Description: Returns the configured intent expiry duration in seconds.
Request: None
Response: uint256 - Intent expiry.
Errors: None
Description: Returns the current aggregator contract address.
Request: None
Response: address - Aggregator address.
Errors: None
Description: Returns the default integrator address.
Request: None
Response: address - Integrator address.
Errors: None
Description: Returns the default integrator fee percentage in basis points.
Request: None
Response: uint64 - Integrator fee.
Errors: None
Description: Returns the configured Alpha tier limit.
Request: None
Response: uint256 - Alpha tier limit.
Errors: None
Description: Returns the configured Beta tier limit.
Request: None
Response: uint256 - Beta tier limit.
Errors: None
Description: Returns the configured Delta tier limit.
Request: None
Response: uint256 - Delta tier limit.
Errors: None
Description: Returns the configured Omega tier limit.
Request: None
Response: uint256 - Omega tier limit.
Errors: None
Description: Returns the configured Titan tier limit.
Request: None
Response: uint256 - Titan tier limit.
Errors: None
Description: Checks if a specific token is currently supported. Request:
address token // Address of the ERC20 token
Response: bool - True if supported, false otherwise.
Errors: None
Description: Checks if a token is supported by the protocol. Request:
address _token // The ERC20 token address to check
Response: bool - True if the token is supported, false otherwise.
Errors: None
Description: Initializes the PGatewaySettings contract with all initial protocol parameters. This function should only be called once.
Request:
PGatewayStructs.InitiateGatewaySettingsParams params // Struct containing all initial settings
// struct InitiateGatewaySettingsParams {
// address initialOwner;
// address treasury;
// address aggregator;
// uint64 protocolFee;
// uint256 alphaLimit;
// uint256 betaLimit;
// uint256 deltaLimit;
// address integrator;
// uint64 integratorFee;
// uint256 omegaLimit;
// uint256 titanLimit;
// uint256 orderExpiryWindow;
// uint256 proposalTimeout;
// uint256 intentExpiry;
// }
Response: No direct return, emits Initialized.
Errors:
InvalidAddress: If any critical address (treasury, aggregator, integrator) isaddress(0).InvalidFee: IfprotocolFeeorintegratorFeeexceeds limits (e.g.,protocolFee > 5000).InvalidLimits: If tier limits are not strictly increasing oralphaLimitis zero.InvalidDuration: IforderExpiryWindow,proposalTimeout, orintentExpiryare zero, orproposalTimeout > orderExpiryWindow.
Description: Updates the protocol fee percentage. Request:
uint64 _newFee // New protocol fee in basis points (max 500 for 5%)
Response: No direct return, emits ProtocolFeeUpdated.
Errors:
Ownable: caller is not the owner: If called by a non-owner.InvalidFee: If_newFeeexceeds500(5%).
Function Type: Transaction setTierLimits(uint256 _alphaLimit, uint256 _betaLimit, uint256 _deltaLimit, uint256 _omegaLimit, uint256 _titanLimit)
Description: Updates all tier limits for order classification. Request:
uint256 _alphaLimit // Max value for Alpha tier
uint256 _betaLimit // Max value for Beta tier
uint256 _deltaLimit // Max value for Delta tier
uint256 _omegaLimit // Max value for Omega tier
uint256 _titanLimit // Max value for Titan tier
Response: No direct return, emits TierLimitsUpdated.
Errors:
Ownable: caller is not the owner: If called by a non-owner.InvalidLimits: If limits are not strictly increasing or_alphaLimitis zero.
Description: Sets the order expiry window duration. Request:
uint256 _newWindow // New expiry window in seconds
Response: No direct return, emits OrderExpiryWindowUpdated.
Errors:
Ownable: caller is not the owner: If called by a non-owner.InvalidDuration: If_newWindowis zero.
Description: Sets the proposal timeout duration. Request:
uint256 _newTimeout // New proposal timeout in seconds
Response: No direct return, emits ProposalTimeoutUpdated.
Errors:
Ownable: caller is not the owner: If called by a non-owner.InvalidDuration: If_newTimeoutis zero.
Description: Updates the treasury address for protocol fee collection. Request:
address _newTreasury // New treasury address
Response: No direct return, emits TreasuryAddressUpdated.
Errors:
Ownable: caller is not the owner: If called by a non-owner.InvalidAddress: If_newTreasuryisaddress(0).
Description: Updates the aggregator address. Request:
address _newAggregator // New aggregator address
Response: No direct return, emits AggregatorAddressUpdated.
Errors:
Ownable: caller is not the owner: If called by a non-owner.InvalidAddress: If_newAggregatorisaddress(0).
Description: Adds or removes a token from the supported tokens list. Request:
address _token // ERC20 token address
bool _supported // True to support, false to remove support
Response: No direct return, emits SupportedTokenUpdated.
Errors:
Ownable: caller is not the owner: If called by a non-owner.InvalidAddress: If_tokenisaddress(0).
Overview: The core settlement engine of the PayNode protocol. It manages the order lifecycle, provider intent registration, settlement proposals, execution, and refunds. This contract is UUPS upgradeable, Pausable, and relies heavily on PayNodeAccessManager for access control and PGatewaySettings for configuration.
Contract Address: [Deployed PGateway Proxy Address]
Description: Returns the address of the IPayNodeAccessManager contract.
Request: None
Response: address - The AccessManager contract address.
Errors: None
Description: Returns the address of the IPGatewaySettings contract.
Request: None
Response: address - The PGatewaySettings contract address.
Errors: None
Description: Retrieves details for a specific order by its ID. Request:
bytes32 orderId // Unique identifier of the order
Response: PGatewayStructs.Order - Order details struct.
Errors: None
Description: Retrieves details for a specific settlement proposal by its ID. Request:
bytes32 proposalId // Unique identifier for the proposal
Response: PGatewayStructs.SettlementProposal - Proposal details struct.
Errors: None
Description: Retrieves the most recent provider intent for a specific provider. Request:
address provider // Address of the liquidity provider
Response: PGatewayStructs.ProviderIntent - Provider intent details struct.
Errors: None
Description: Retrieves reputation data for a specific provider. Request:
address provider // Address of the liquidity provider
Response: PGatewayStructs.ProviderReputation - Provider reputation details struct.
Errors: None
Description: Returns the current nonce for a specific user. Request:
address user // Address of the user
Response: uint256 - Current user nonce.
Errors: None
Description: Checks if a settlement proposal has already been executed. Request:
bytes32 proposalId // Unique identifier for the proposal
Response: bool - True if executed, false otherwise.
Errors: None
Description: Retrieves information for a registered integrator. Request:
address integrator // Address of the integrator
Response: PGatewayStructs.IntegratorInfo - Integrator info struct.
Errors: None
Description: Checks if a message hash has been used to prevent replay attacks. Request:
bytes32 messageHash // The message hash to check
Response: bool - True if used, false otherwise.
Errors: None
Description: Returns the maximum allowed integrator fee in basis points.
Request: None
Response: uint64 - Maximum integrator fee.
Errors: None
Description: Returns the minimum allowed integrator fee in basis points.
Request: None
Response: uint64 - Minimum integrator fee.
Errors: None
Description: Initializes the PGateway contract, setting up references to the PayNodeAccessManager and PGatewaySettings contracts. This function should only be called once.
Request:
address _accessManager // Address of the PayNodeAccessManager contract
address _settings // Address of the PGatewaySettings contract
Response: No direct return. Errors:
InvalidAddress: If_accessManageror_settingsisaddress(0).
Description: Pauses the contract, halting most operations. Requires DEFAULT_ADMIN_ROLE and executes through AccessManager's non-reentrant mechanism.
Request: None
Response: No direct return, emits Paused.
Errors:
Unauthorized: Ifmsg.senderdoes not haveDEFAULT_ADMIN_ROLEorexecuteNonReentrantfails.
Description: Unpauses the contract, resuming normal operations. Requires DEFAULT_ADMIN_ROLE and executes through AccessManager's non-reentrant mechanism.
Request: None
Response: No direct return, emits Unpaused.
Errors:
Unauthorized: Ifmsg.senderdoes not haveDEFAULT_ADMIN_ROLEorexecuteNonReentrantfails.
Function Type: Transaction registerIntent(string calldata _currency, uint256 _availableAmount, uint64 _minFeeBps, uint64 _maxFeeBps, uint256 _commitmentWindow)
Description: Registers a provider's intent to offer liquidity, specifying currency, amount, fee range, and commitment window. Request:
string _currency // Currency code (e.g., "USDT", "NGN")
uint256 _availableAmount // Amount provider can handle
uint64 _minFeeBps // Minimum acceptable fee in basis points
uint64 _maxFeeBps // Maximum acceptable fee in basis points
uint256 _commitmentWindow // Time window for provider to accept proposal
Response: No direct return, emits IntentRegistered.
Errors:
Pausable: paused: If the contract is paused.UserBlacklisted: Ifmsg.senderis blacklisted by AccessManager.InvalidProvider: IfexecuteProviderNonReentrantfails.InvalidAmount: If_availableAmountis zero.InvalidFee: If_minFeeBps > _maxFeeBpsor_maxFeeBps > settings.maxProtocolFee().InvalidDuration: If_commitmentWindowis zero.ErrorProviderBlacklisted: If the provider is blacklisted inproviderReputation.
Description: Updates an existing provider's available liquidity amount. Request:
string _currency // Currency code
uint256 _newAmount // New available amount
Response: No direct return, emits IntentUpdated.
Errors:
NotRegisteredProvider: Ifmsg.senderis not a registered provider.Pausable: paused: If the contract is paused.ErrorProviderBlacklisted: IfexecuteProviderNonReentrantfails.InvalidAmount: If_newAmountis zero.InvalidIntent: If the provider's intent is not active.
Description: Expires a provider's intent, setting it to inactive. Can be called manually by aggregator if intent expiry is past. Request:
address _provider // Address of the provider whose intent to expire
Response: No direct return, emits IntentExpired.
Errors:
Unauthorized: Ifmsg.senderdoes not haveAGGREGATOR_ROLEorexecuteAggregatorNonReentrantfails.InvalidIntent: If the provider's intent is not active.IntentNotExpired: Ifblock.timestampis not yet pastintent.expiresAt.
Description: Reserves a portion of a provider's available capacity when a proposal is sent. Request:
address _provider // Provider address
uint256 _amount // Amount to reserve
Response: No direct return. Errors:
Unauthorized: Ifmsg.senderdoes not haveAGGREGATOR_ROLEorexecuteAggregatorNonReentrantfails.InvalidIntent: If the provider's intent is not active.InvalidAmount: Ifintent.availableAmount < _amount.
Function Type: Transaction releaseIntent(address _provider, uint256 _amount, string calldata _reason)
Description: Releases previously reserved capacity back to a provider, typically if a proposal is rejected or times out. Request:
address _provider // Provider address
uint256 _amount // Amount to release
string _reason // Reason for releasing capacity
Response: No direct return, emits IntentReleased.
Errors:
Unauthorized: Ifmsg.senderdoes not haveAGGREGATOR_ROLEorexecuteAggregatorNonReentrantfails.
Description: Retrieves the active provider intent details for a given provider. Request:
address _provider // Provider address
Response: PGatewayStructs.ProviderIntent - Struct containing intent details.
Errors: None
Description: Allows any address to register as an integrator with a custom fee and name. Request:
uint64 _feeBps // Desired fee in basis points (e.g., 100 = 1%)
string _name // Integrator's display name (max 50 chars)
Response: No direct return, emits IntegratorRegistered.
Errors:
AlreadyRegistered: Ifmsg.senderis already a registered integrator.FeeOutOfRange: If_feeBpsis outsideMIN_INTEGRATOR_FEEandMAX_INTEGRATOR_FEE.InvalidName: If_nameis empty or too long.
Description: Allows a registered integrator to update their fee configuration. Request:
uint64 _newFeeBps // New fee in basis points
Response: No direct return, emits IntegratorFeeUpdated.
Errors:
NotRegistered: Ifmsg.senderis not a registered integrator.FeeOutOfRange: If_newFeeBpsis outsideMIN_INTEGRATOR_FEEandMAX_INTEGRATOR_FEE.
Description: Allows a registered integrator to update their display name. Request:
string _newName // New name for the integrator (max 50 chars)
Response: No direct return, emits IntegratorNameUpdated.
Errors:
NotRegistered: Ifmsg.senderis not a registered integrator.InvalidName: If_newNameis empty or too long.
Function Type: Transaction createOrder(address _token, uint256 _amount, address _refundAddress, address _integrator, uint64 _integratorFee, bytes32 _messageHash)
Description: Initiates a new payment order. Transfers tokens from the user to the contract's escrow. Request:
address _token // ERC20 token address
uint256 _amount // Order amount in tokens
address _refundAddress // Address for refunds
address _integrator // Address of the dApp/integrator
uint64 _integratorFee // Integrator's fee in basis points for this order
bytes32 _messageHash // Unique hash of off-chain order details
Response: bytes32 - The unique orderId. Emits OrderCreated.
Errors:
Pausable: paused: If the contract is paused.UserBlacklisted: Ifmsg.senderis blacklisted by AccessManager.TokenNotSupported: If_tokenis not supported byPGatewaySettings.InvalidAmount: If_amountis zero.InvalidAddress: If_refundAddressor_integratorisaddress(0).InvalidMessageHash: If_messageHashis empty.Unauthorized: IfexecuteNonReentrantfails.MessageHashAlreadyUsed: If_messageHashhas been used before.SafeERC20: ERC20: transfer amount exceeds balance: If user has insufficient balance.SafeERC20: ERC20: transferFrom failed: If transfer from user fails (e.g., allowance not set).
Description: Retrieves the full details of a specific order. Request:
bytes32 _orderId // Unique identifier of the order
Response: PGatewayStructs.Order - Order details struct.
Errors:
OrderNotFound: If_orderIddoes not correspond to an existing order.
Function Type: Transaction createProposal(bytes32 _orderId, address _provider, uint64 _proposedFeeBps)
Description: An aggregator creates a settlement proposal for a pending order, selecting a provider and specifying a fee. Request:
bytes32 _orderId // Associated order ID
address _provider // Address of the chosen provider
uint64 _proposedFeeBps // Proposed fee in basis points
Response: bytes32 - The unique proposalId. Emits SettlementProposalCreated.
Errors:
Unauthorized: Ifmsg.senderdoes not haveAGGREGATOR_ROLEorexecuteAggregatorNonReentrantfails.OrderNotFound: If_orderIddoes not correspond to an existing order.InvalidOrder: If the order is not inPENDINGstatus.OrderExpired: If the order'sexpiresAttimestamp has passed.InvalidIntent: If the provider's intent is not active.InvalidAmount: IfproviderIntents[_provider].availableAmount < order.amount.InvalidFee: If_proposedFeeBpsis outside the provider'sminFeeBpsandmaxFeeBps.
Description: A provider accepts a settlement proposal, committing to fulfill the order. Request:
bytes32 _proposalId // Proposal ID to accept
Response: No direct return, emits SettlementProposalAccepted.
Errors:
NotRegisteredProvider: Ifmsg.senderis not a registered provider.Pausable: paused: If the contract is paused.InvalidProvider: IfexecuteProviderNonReentrantfails.Unauthorized: Ifmsg.senderis not theproviderassociated with_proposalId.InvalidProposal: If the proposal is not inPENDINGstatus or has expired.
Description: A provider rejects a settlement proposal, releasing their reserved capacity. Request:
bytes32 _proposalId // Proposal ID to reject
string _reason // Reason for rejection
Response: No direct return, emits SettlementProposalRejected.
Errors:
NotRegisteredProvider: Ifmsg.senderis not a registered provider.InvalidProvider: IfexecuteProviderNonReentrantfails.Unauthorized: Ifmsg.senderis not theproviderassociated with_proposalId.InvalidProposal: If the proposal is not inPENDINGstatus.
Description: Marks a settlement proposal as timed out if the deadline has passed without acceptance. Request:
bytes32 _proposalId // Proposal ID to mark as timed out
Response: No direct return, emits SettlementProposalTimeout.
Errors:
Unauthorized: Ifmsg.senderdoes not haveAGGREGATOR_ROLEorexecuteAggregatorNonReentrantfails.InvalidProposal: If the proposal is not inPENDINGstatus or has not yet reached its deadline.
Description: Retrieves the full details of a specific settlement proposal. Request:
bytes32 _proposalId // Proposal identifier
Response: PGatewayStructs.SettlementProposal - Struct with proposal details.
Errors: None
Description: Executes an accepted settlement, distributing funds from escrow to treasury, integrator, and provider. Request:
bytes32 _proposalId // The ID of the accepted proposal to execute settlement for
Response: No direct return, emits SettlementExecuted.
Errors:
Unauthorized: Ifmsg.senderdoes not haveAGGREGATOR_ROLEorexecuteAggregatorNonReentrantfails.InvalidProposal: If the proposal is not inACCEPTEDstatus or has already been executed.InvalidOrder: If the associated order is not inACCEPTEDstatus.SafeERC20: ERC20: transfer amount exceeds balance: If contract has insufficient balance.SafeERC20: ERC20: transfer failed: If transfer to treasury, integrator, or provider fails.
Description: Refunds an order if no provider accepts within the order expiry window. Request:
bytes32 _orderId // Order ID to refund
Response: No direct return, emits OrderRefunded.
Errors:
Unauthorized: Ifmsg.senderdoes not haveAGGREGATOR_ROLEorexecuteAggregatorNonReentrantfails.OrderNotFound: If_orderIddoes not correspond to an existing order.InvalidOrder: If the order is alreadyFULFILLEDorREFUNDED.OrderNotExpired: Ifblock.timestampis not yet pastorder.expiresAt.SafeERC20: ERC20: transfer amount exceeds balance: If contract has insufficient balance.SafeERC20: ERC20: transfer failed: If transfer to refund address fails.
Description: Allows a user to manually request a refund if their order has not been fulfilled and has expired. Request:
bytes32 _orderId // Order ID to refund
Response: No direct return, emits OrderRefunded.
Errors:
OrderNotFound: If_orderIddoes not correspond to an existing order.Unauthorized: Ifmsg.senderis not theuserof the order, or ifexecuteNonReentrantfails.InvalidOrder: If the order is inFULFILLEDorREFUNDEDstatus.OrderNotExpired: Ifblock.timestampis not yet pastorder.expiresAt.SafeERC20: ERC20: transfer amount exceeds balance: If contract has insufficient balance.SafeERC20: ERC20: transfer failed: If transfer to refund address fails.
Description: Flags a provider as fraudulent, disabling their intent and reputation. Request:
address _provider // Provider address to flag
Response: No direct return, emits ProviderFraudFlagged.
Errors:
Unauthorized: Ifmsg.senderdoes not haveAGGREGATOR_ROLEorexecuteAggregatorNonReentrantfails.InvalidAddress: If_providerisaddress(0).
Description: Blacklists a provider, preventing them from participating in settlements. Requires DEFAULT_ADMIN_ROLE.
Request:
address _provider // Provider address to blacklist
string _reason // Reason for blacklisting
Response: No direct return, emits ProviderBlacklisted.
Errors:
Unauthorized: Ifmsg.senderdoes not haveDEFAULT_ADMIN_ROLEorexecuteNonReentrantfails.
Description: Retrieves a provider's reputation data. Request:
address _provider // Provider address
Response: PGatewayStructs.ProviderReputation - Struct with reputation metrics.
Errors: None
Description: Returns a user's current nonce for replay protection. Request:
address _user // User address
Response: uint256 - Current nonce.
Errors: None
Description: Retrieves complete information for a registered integrator. Request:
address _integrator // Address of the integrator
Response: PGatewayStructs.IntegratorInfo - Complete integrator information.
Errors: None
Overview: The governance contract responsible for managing timelocked upgrades of proxy contracts and scheduled role changes within the PayNode ecosystem. It extends OpenZeppelin's TimelockController and integrates Chainlink Automation for automated upkeep.
Contract Address: [Deployed PayNodeAdmin Address]
Description: Returns the bytes32 identifier for the ADMIN_ROLE specific to this contract, which can schedule and execute upgrades.
Request: None
Response: bytes32 - The role identifier.
Errors: None
Description: Returns the minimum delay (in seconds) that must pass between scheduling and executing critical operations.
Request: None
Response: uint256 - The value 2 days (172800 seconds).
Errors: None
Description: Returns the cooldown period (in seconds) between performUpkeep calls by Chainlink Automation.
Request: None
Response: uint256 - The value 1 hour (3600 seconds).
Errors: None
Description: Returns the immutable address of the Chainlink Keeper authorized to call performUpkeep.
Request: None
Response: address - The Chainlink Keeper address.
Errors: None
Description: Returns the timestamp of the last successful performUpkeep execution.
Request: None
Response: uint256 - Last upkeep timestamp.
Errors: None
Description: Retrieves details of a pending upgrade for a specific proxy target. Request:
address target // The proxy contract address
Response: PGatewayStructs.PendingUpgrade - Struct containing upgrade details.
Errors: None
Description: Retrieves a proxy address from the upgrade queue by its index. Request:
uint256 index // Index in the upgradeQueue array
Response: address - Proxy contract address.
Errors: None (will revert if index out of bounds)
Description: Retrieves details of a pending role change operation. Request:
bytes32 operationId // Unique identifier of the role change
Response: PGatewayStructs.PendingRoleChange - Struct containing role change details.
Errors: None
Description: Returns the array of all proxy addresses currently in the upgrade queue.
Request: None
Response: address[] memory - Array of addresses with pending upgrades.
Errors: None
Description: Checks if a scheduled role change operation is ready for execution (i.e., timelock has passed). Request:
bytes32 operationId // The operation ID to check
Response: bool - True if ready, false otherwise.
Errors: None
Function Type: Constructor constructor(address[] memory proposers, address[] memory executors, address superAdmin, address upgradeAdmin, address _chainlinkKeeper)
Description: Initializes the PayNodeAdmin contract with roles for proposers, executors, super admin, upgrade admin, and the Chainlink Keeper.
Request:
address[] proposers // Addresses allowed to propose timelocked operations
address[] executors // Addresses allowed to execute timelocked operations
address superAdmin // Address to receive DEFAULT_ADMIN_ROLE and act as admin
address upgradeAdmin // Address to receive ADMIN_ROLE (for upgrades)
address _chainlinkKeeper // Address of the Chainlink Keeper for automation
Response: No direct return. Errors:
InvalidAddress: If_chainlinkKeeperisaddress(0).
Description: Schedules an upgrade for a proxy contract. This operation is timelocked. Request:
address target // The proxy contract to upgrade
address newImplementation // The new implementation contract address
Response: No direct return, emits UpgradeScheduled.
Errors:
AccessControl: sender missing role(ADMIN_ROLE)Pausable: paused: If the contract is paused.InvalidAddress: IftargetornewImplementationisaddress(0).UpgradeAlreadyPending: If an upgrade is already pending for thistarget.ValueTooLargeForuint96: Ifblock.timestamp + MIN_DELAYexceedstype(uint96).max.
Description: Cancels a previously scheduled upgrade for a proxy contract. Request:
address target // The proxy contract whose upgrade to cancel
Response: No direct return, emits UpgradeCancelled.
Errors:
AccessControl: sender missing role(ADMIN_ROLE)Pausable: paused: If the contract is paused.NoUpgradePending: If no upgrade is pending for thistarget.
Description: Manually executes a scheduled upgrade for a proxy contract after its timelock has passed. Request:
address target // The proxy contract to upgrade
Response: No direct return, emits UpgradeExecuted.
Errors:
AccessControl: sender missing role(ADMIN_ROLE)Pausable: paused: If the contract is paused.NoUpgradePending: If no upgrade is pending for thistarget.UpgradeTooEarly: If the timelock period has not yet passed.UpgradeFailed: If the low-levelupgradeTocall on the proxy fails.
Description: Chainlink Automation callback: Checks if any pending upgrade is ready for execution. Request:
bytes checkData // (ignored)
Response: (bool upkeepNeeded, bytes memory performData)
upkeepNeeded: True if an upgrade is ready.performData: Encodedtargetaddress ifupkeepNeededis true. Errors: None
Description: Chainlink Automation callback: Executes a ready upgrade. Restricted to the Chainlink Keeper. Request:
bytes performData // Encoded target address from checkUpkeep
Response: No direct return, emits UpkeepPerformed and UpgradeExecuted.
Errors:
OnlyChainlinkKeeper: If called by an address other thanchainlinkKeeper.Pausable: paused: If the contract is paused.UpkeepCooldownActive: If called within theUPKEEP_COOLDOWNperiod.NoUpgradePending: If no upgrade is pending for the decodedtarget.UpgradeTooEarly: If the timelock period for the upgrade has not yet passed.UpgradeFailed: If the low-levelupgradeTocall on the proxy fails.
Description: Schedules a role assignment or revocation with a timelock delay. Request:
address account // The account to modify
bytes32 role // The role to grant or revoke
bool grant // True to grant, false to revoke
Response: No direct return, emits RoleChangeScheduled.
Errors:
AccessControl: sender missing role(ADMIN_ROLE)Pausable: paused: If the contract is paused.InvalidAddress: Ifaccountisaddress(0).ValueTooLargeForuint96: Ifblock.timestamp + MIN_DELAYexceedstype(uint96).max.
Description: Executes a pending role change after its timelock period has passed. Request:
bytes32 operationId // Unique identifier of the pending role change
Response: No direct return, emits RoleChangeExecuted.
Errors:
AccessControl: sender missing role(DEFAULT_ADMIN_ROLE)Pausable: paused: If the contract is paused.RoleChangeNotReady: IfoperationIdis invalid, not found, or timelock has not passed.
Description: Cancels a pending role change operation. Request:
bytes32 operationId // The operation ID to cancel
Response: No direct return, emits RoleChangeScheduled with scheduleTime = 0.
Errors:
AccessControl: sender missing role(DEFAULT_ADMIN_ROLE)Pausable: paused: If the contract is paused.RoleChangeNotReady: IfoperationIdis invalid or not found.
Description: Pauses the contract, halting all critical operations. Restricted to DEFAULT_ADMIN_ROLE.
Request: None
Response: No direct return, emits Paused.
Errors:
AccessControl: sender missing role(DEFAULT_ADMIN_ROLE)
Description: Unpauses the contract, resuming all operations. Restricted to DEFAULT_ADMIN_ROLE.
Request: None
Response: No direct return, emits Unpaused.
Errors:
AccessControl: sender missing role(DEFAULT_ADMIN_ROLE)
The PayNode protocol contracts are designed to be integrated by off-chain aggregators and dApps (integrators) to facilitate non-custodial off-ramp payment settlements.
- User Approves Token: User approves the
PGatewaycontract to spend their ERC20 token for the order amount.IERC20(tokenAddress).approve(gatewayAddress, amount);
- User Creates Order: The user (or the dApp on their behalf) calls
PGateway.createOrder(), specifying the token, amount, refund address, integrator details, and a unique_messageHash(linking to off-chain order details like bank account information).This transfers the tokens to the// Example: Create an order for 100 USDC to be off-ramped // Assuming USDC is 6 decimal places for example, so amount is 100 * 10^6 gateway.createOrder( USDC_ADDRESS, 100_000_000, // 100 USDC userRefundAddress, integratorAddress, integratorFeeBps, // e.g., 50 for 0.5% keccak256(abi.encodePacked("unique_off_chain_order_id_user_data_123")) );
PGatewaycontract's escrow.
- Provider Registers/Updates Intent: Providers register their intent to offer liquidity using
PGateway.registerIntent()orPGateway.updateIntent(). They specify the currency, available amount, fee range, and commitment window.// Example: Provider registers intent for 50,000 NGN equivalent liquidity gateway.registerIntent( "NGN", 50_000_000_000_000_000_000, // Example for 18-decimal NGN equivalent 200, // 2% min fee 500, // 5% max fee 30 // 30-second commitment window );
- Provider Accepts Proposal: Once an off-chain aggregator sends a proposal for a matching order, the provider can accept it using
PGateway.acceptProposal().gateway.acceptProposal(proposalId); - Provider Rejects Proposal: If a provider cannot fulfill a proposal, they can reject it using
PGateway.rejectProposal().gateway.rejectProposal(proposalId, "Insufficient fiat liquidity");
- Monitor Orders: The off-chain aggregator monitors
OrderCreatedevents fromPGatewayand tracks available provider intents viaPGateway.getProviderIntent(). - Route and Create Proposals: Based on order tiers, provider reputation, and capacity, the aggregator identifies eligible providers and sends settlement proposals using
PGateway.createProposal(). This is done in parallel for multiple providers.// Example: Aggregator proposes settlement to Provider A for an order gateway.createProposal( orderId, providerA_Address, 300 // 3% proposed fee );
- Execute Settlement: Once a provider accepts a proposal, the aggregator calls
PGateway.executeSettlement()to finalize the transaction, distributing fees and sending funds to the provider (who then executes the off-chain fiat payment to the user).gateway.executeSettlement(acceptedProposalId); - Manage Timeouts/Refunds: The aggregator is responsible for calling
PGateway.timeoutProposal()for expired proposals andPGateway.refundOrder()for orders that expire without a successful settlement.
- System Configuration: The contract owner (or
DEFAULT_ADMIN_ROLEviaPayNodeAccessManager) configures protocol parameters throughPGatewaySettings(e.g.,setProtocolFee(),setTierLimits(),setSupportedToken()). These changes are often timelocked viaPayNodeAdmin. - Upgrade Management: New
PGatewayorPayNodeAccessManagerimplementations are scheduled for upgrade by theADMIN_ROLEholder viaPayNodeAdmin.scheduleUpgrade(). After theMIN_DELAY(2 days), the upgrade can be executed manually or automatically by Chainlink Automation viaPayNodeAdmin.performUpgrade()orPayNodeAdmin.performUpkeep(). - Emergency Control: The
DEFAULT_ADMIN_ROLEcan invokePayNodeAccessManager.emergencyShutdown()to pause all critical operations if a vulnerability is detected.
| Technology | Description |
|---|---|
| Solidity | The primary programming language for writing smart contracts on the Ethereum Virtual Machine. |
| Foundry | A blazing-fast, portable, and modular toolkit for Ethereum application development, used for compiling, testing, and deploying contracts. |
| OpenZeppelin Contracts | Industry-standard library for secure smart contract development, providing battle-tested implementations of ERC standards, access control, and upgradeability patterns. |
| OpenZeppelin Upgradeable Contracts | Specialized versions of OpenZeppelin contracts designed for upgradeability using patterns like UUPS proxies. |
| Chainlink Automation | Decentralized oracle network service used here for reliably triggering smart contract functions (e.g., executing timelocked upgrades) based on predefined conditions. |
| ERC1967Proxy (UUPS) | An upgradeability pattern that allows contracts to be upgraded by simply changing their implementation address, preserving the contract's address and state. |
| TimelockController | A contract that enforces a time delay for critical operations, enhancing security by providing a window for review before potentially malicious or erroneous actions are executed. |
| AccessControl | A role-based access control mechanism, allowing precise management of permissions for different functions and users within the system. |
| Pausable | A utility contract that provides an emergency stop mechanism, allowing authorized roles to pause and unpause contract functionality. |
| ReentrancyGuard | A security mechanism that prevents reentrancy attacks, a common vulnerability where external calls can "re-enter" a contract before the initial call is finished, leading to unexpected behavior. |
We welcome contributions to the PayNode Protocol! To contribute, please follow these guidelines:
- β¨ Fork the Repository: Start by forking the
paynode-contractrepository to your GitHub account. - πΏ Create a New Branch: Create a new branch for your feature or bug fix:
git checkout -b feature/your-feature-nameorbugfix/issue-description. - π Implement Your Changes: Write clean, maintainable, and well-tested code. Ensure your changes adhere to the existing code style.
- π§ͺ Run Tests: Before submitting, ensure all existing tests pass and add new tests for your changes. Foundry tests can be run with
forge test. - π Update Documentation: If your changes introduce new functionality or modify existing APIs, please update the relevant documentation.
- β¬οΈ Commit Your Changes: Commit your changes with a clear and concise message.
- π¬ Open a Pull Request: Submit a pull request to the
mainbranch of the original repository. Provide a detailed description of your changes and why they are necessary.
This project is licensed under the MIT License. See the LICENSE file for details.
Connect with the author of this project!
- Twitter: @olujimi_the_dev