Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
0ec1761
feat: add ERC20RecurringPaymentProxy contract for executing recurring…
aimensahnoun Jun 16, 2025
e946bbd
feat: integrate ERC20RecurringPaymentProxy deployment into test scrip…
aimensahnoun Jun 16, 2025
41b25ca
fix(ERC20RecurringPaymentProxy): correct execution index validation a…
aimensahnoun Jun 18, 2025
ab83d32
feat(ERC20RecurringPaymentProxy): implement Ownable pattern for enhan…
aimensahnoun Jun 18, 2025
c50569e
test(ERC20RecurringPaymentProxy): add comprehensive test suite for ER…
aimensahnoun Jun 18, 2025
5f534ba
refactor(ERC20RecurringPaymentProxy.test): streamline imports and rem…
aimensahnoun Jun 18, 2025
f738b0a
test(ERC20RecurringPaymentProxy): enhance test suite with execution s…
aimensahnoun Jun 18, 2025
0733e35
test(ERC20RecurringPaymentProxy): update signature generation to use …
aimensahnoun Jun 19, 2025
5e38fb9
test(ERC20RecurringPaymentProxy): improve signature generation to sup…
aimensahnoun Jun 19, 2025
318a8c5
test(ERC20RecurringPaymentProxy): add snapshot management for consist…
aimensahnoun Jun 19, 2025
97349ab
test:skip sequential test to debug failing CI
aimensahnoun Jun 20, 2025
b59f3ea
test(ERC20RecurringPaymentProxy): re-enable sequential payment execut…
aimensahnoun Jun 20, 2025
0e306bd
feat: setup necessary scripts for ERC20RecurringPaymentProxy deployment
aimensahnoun Jun 20, 2025
1baf1a6
feat(ERC20RecurringPaymentProxy): add function to retrieve ERC-20 all…
aimensahnoun Jun 20, 2025
15ac8f1
feat(ERC20RecurringPaymentProxy): add function to encode transaction …
aimensahnoun Jun 20, 2025
ab384c9
feat(ERC20RecurringPaymentProxy): implement functions for decreasing …
aimensahnoun Jun 20, 2025
345d4f3
test(ERC20RecurringPaymentProxy): add comprehensive test suite for re…
aimensahnoun Jun 20, 2025
e1b7d99
test(ERC20RecurringPaymentProxy): refactor allowance spy implementati…
aimensahnoun Jun 20, 2025
2f8fd85
test(ERC20RecurringPaymentProxy): enhance tests for encoding approval…
aimensahnoun Jun 20, 2025
2b80af8
test(ERC20RecurringPaymentProxy): remove redundant allowance tests an…
aimensahnoun Jun 20, 2025
001b91c
test(ERC20RecurringPaymentProxy): update test suite to use dynamic wa…
aimensahnoun Jun 23, 2025
e21ad15
fix(ERC20RecurringPaymentProxy): update error message to specify netw…
aimensahnoun Jun 23, 2025
ad40443
chore(ERC20RecurringPaymentProxy): remove unused overrides parameter …
aimensahnoun Jun 23, 2025
67dd8e5
refactor(ERC20RecurringPaymentProxy): simplify _hashSchedule function…
aimensahnoun Jun 23, 2025
3f56cc4
docs(ERC20RecurringPaymentProxy): update documentation to reflect cha…
aimensahnoun Jun 23, 2025
ac33544
refactor(ERC20RecurringPaymentProxy): rename gasFee to executorFee in…
aimensahnoun Jun 24, 2025
4597b8d
feat(ERC20RecurringPaymentProxy): implement USDT-specific approval an…
aimensahnoun Jun 24, 2025
012578a
refactor(ERC20RecurringPaymentProxy): consolidate approval methods in…
aimensahnoun Jun 25, 2025
7a9c2a2
test(ERC20RecurringPaymentProxy): add afterEach hook to restore mocks…
aimensahnoun Jun 25, 2025
e8f35fd
Merge branch 'master' into feat/ERC20-recurring-payment-proxy
aimensahnoun Jun 29, 2025
187b0f1
Merge branch 'master' of github.com:RequestNetwork/requestNetwork int…
aimensahnoun Jul 1, 2025
8863dce
Merge branch 'feat/ERC20-recurring-payment-proxy' of github.com:Reque…
aimensahnoun Jul 1, 2025
360de5f
refactor(constructor-args): extract environment variable retrieval in…
aimensahnoun Jul 2, 2025
1e259e4
feat(ERC20RecurringPaymentProxy): add strictOrder parameter to schedu…
aimensahnoun Jul 2, 2025
3f11e0b
feat(payment-types): add strictOrder property to SchedulePermit inter…
aimensahnoun Jul 2, 2025
a2a8e66
test(erc-20-recurring-payment): add strictOrder property to ScheduleP…
aimensahnoun Jul 2, 2025
c066310
fix(ERC20RecurringPaymentProxy): correct Solidity version declaration
aimensahnoun Jul 2, 2025
9d45f29
test(ERC20RecurringPaymentProxy): comment out out-of-order execution …
aimensahnoun Jul 2, 2025
f6ed717
refactor(ERC20RecurringPaymentProxy): rename execution functions and …
aimensahnoun Jul 3, 2025
098c985
test(ERC20RecurringPaymentProxy): update test cases to use BigNumber …
aimensahnoun Jul 3, 2025
6b390ae
refactor(ERC20RecurringPaymentProxy): reorganize signer variable decl…
aimensahnoun Jul 3, 2025
02c0f27
test(ERC20RecurringPaymentProxy): update tests to expect generic reve…
aimensahnoun Jul 3, 2025
f676170
refactor(ERC20RecurringPaymentProxy): adjust signing logic to improve…
aimensahnoun Jul 3, 2025
799ea21
refactor(ERC20RecurringPaymentProxy): rename firstExec to firstPaymen…
aimensahnoun Jul 3, 2025
ef71174
refactor(ERC20RecurringPaymentProxy): update SchedulePermit to use Bi…
aimensahnoun Jul 3, 2025
7efe903
refactor(ERC20RecurringPaymentProxy): rename triggerRecurringPayment …
aimensahnoun Jul 3, 2025
905fd0f
refactor(ERC20RecurringPaymentProxy): rename executor-related terms t…
aimensahnoun Jul 3, 2025
55f9e8b
refactor(ERC20RecurringPaymentProxy): rename firstExec to firstPaymen…
aimensahnoun Jul 3, 2025
cd19cae
feat(ERC20RecurringPaymentProxy): add new recurring payment proxy fun…
aimensahnoun Jul 3, 2025
c5b0e1a
fix(ERC20RecurringPaymentProxy): change return type of triggerRecurri…
aimensahnoun Jul 4, 2025
96984d8
chore: update dependencies and configuration for hardhat-verify integ…
aimensahnoun Jul 4, 2025
37f106e
chore: pin hardhat-verify dependency version to 2.0.14 for consistency
aimensahnoun Jul 4, 2025
3fa1f5c
chore: update typescript version to 4.8.4 and adjust dependencies in …
aimensahnoun Jul 4, 2025
69a41b0
feat(ERC20RecurringPaymentProxy): implement EIP-712 signature generat…
aimensahnoun Jul 4, 2025
fc87fd2
feat(ERC20RecurringPaymentProxy): add base contract address and creat…
aimensahnoun Jul 4, 2025
9c3dfa6
test(ERC20RecurringPayment): skip recurring payment test for further …
aimensahnoun Jul 5, 2025
a641515
test(ERC20RecurringPayment): implement and enhance recurring payment …
aimensahnoun Jul 5, 2025
0a8eb3a
fix(ERC20RecurringPaymentProxy): update documentation for triggerRecu…
aimensahnoun Jul 5, 2025
0bea365
test(ERC20RecurringPaymentProxy): re-enable out of order execution te…
aimensahnoun Jul 5, 2025
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
Prev Previous commit
Next Next commit
refactor(ERC20RecurringPaymentProxy): rename execution functions and …
…update related comments and tests for clarity
  • Loading branch information
aimensahnoun committed Jul 3, 2025
commit f6ed717e1eb6137a67aa5c4fd0f140c282c1495a
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export async function getPayerRecurringPaymentAllowance({
* @param network - The EVM chain name where the proxy is deployed
* @param isUSDT - Flag to indicate if the token is USDT, which requires special handling
*
* @returns Array of transaction objects ready to be executed by a wallet
* @returns Array of transaction objects ready to be sent to the blockchain
*
* @throws {Error} If the ERC20RecurringPaymentProxy is not deployed on the specified network
*
Expand Down Expand Up @@ -104,24 +104,24 @@ export function encodeSetRecurringAllowance({
}

/**
* Encodes the transaction data to execute a recurring payment through the ERC20RecurringPaymentProxy.
* Encodes the transaction data to trigger a recurring payment through the ERC20RecurringPaymentProxy.
*
* @param permitTuple - The SchedulePermit struct data
* @param permitSignature - The signature authorizing the recurring payment schedule
* @param paymentIndex - The index of the payment to execute (1-based)
* @param paymentReference - Reference data for the payment execution
* @param paymentIndex - The index of the payment to trigger (1-based)
* @param paymentReference - Reference data for the payment
* @param network - The EVM chain name where the proxy is deployed
*
* @returns The encoded function data as a hex string, ready to be used in a transaction
*
* @throws {Error} If the ERC20RecurringPaymentProxy is not deployed on the specified network
*
* @remarks
* β€’ The function only encodes the transaction data without executing it
* β€’ The function only encodes the transaction data without sending it
* β€’ The encoded data can be used with any web3 library or multisig wallet
* β€’ Make sure the paymentIndex matches the expected execution sequence
* β€’ Make sure the paymentIndex matches the expected payment sequence
*/
export function encodeRecurringPaymentExecution({
export function encodeRecurringPaymentTrigger({
permitTuple,
permitSignature,
paymentIndex,
Expand All @@ -138,7 +138,7 @@ export function encodeRecurringPaymentExecution({
}): string {
const proxyContract = erc20RecurringPaymentProxyArtifact.connect(network, provider);

return proxyContract.interface.encodeFunctionData('execute', [
return proxyContract.interface.encodeFunctionData('triggerRecurringPayment', [
permitTuple,
permitSignature,
paymentIndex,
Expand All @@ -147,13 +147,13 @@ export function encodeRecurringPaymentExecution({
}

/**
* Executes a recurring payment through the ERC20RecurringPaymentProxy.
* Triggers a recurring payment through the ERC20RecurringPaymentProxy.
*
* @param permitTuple - The SchedulePermit struct data
* @param permitSignature - The signature authorizing the recurring payment schedule
* @param paymentIndex - The index of the payment to execute (1-based)
* @param paymentReference - Reference data for the payment execution
* @param signer - The signer that will execute the transaction (must have EXECUTOR_ROLE)
* @param paymentIndex - The index of the payment to trigger (1-based)
* @param paymentReference - Reference data for the payment
* @param signer - The signer that will trigger the transaction (must have RELAYER_ROLE)
* @param network - The EVM chain name where the proxy is deployed
*
* @returns A Promise resolving to the transaction receipt after the payment is confirmed
Expand All @@ -163,10 +163,10 @@ export function encodeRecurringPaymentExecution({
*
* @remarks
* β€’ The function waits for the transaction to be mined before returning
* β€’ The signer must have been granted EXECUTOR_ROLE by the proxy admin
* β€’ The signer must have been granted RELAYER_ROLE by the proxy admin
* β€’ Make sure all preconditions are met (allowance, balance, timing) before calling
*/
export async function executeRecurringPayment({
export async function triggerRecurringPayment({
permitTuple,
permitSignature,
paymentIndex,
Expand All @@ -183,7 +183,7 @@ export async function executeRecurringPayment({
}): Promise<providers.TransactionReceipt> {
const proxyAddress = getRecurringPaymentProxyAddress(network);

const data = encodeRecurringPaymentExecution({
const data = encodeRecurringPaymentTrigger({
permitTuple,
permitSignature,
paymentIndex,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Wallet, providers } from 'ethers';
import { erc20RecurringPaymentProxyArtifact } from '@requestnetwork/smart-contracts';
import { CurrencyTypes, PaymentTypes } from '@requestnetwork/types';
import { Wallet, providers } from 'ethers';
import {
encodeRecurringPaymentTrigger,
encodeSetRecurringAllowance,
encodeRecurringPaymentExecution,
executeRecurringPayment,
triggerRecurringPayment,
} from '../../src/payment/erc20-recurring-payment-proxy';

const mnemonic = 'candy maple cake sugar pudding cream honey rich smooth crumble sweet treat';
Expand All @@ -20,10 +20,10 @@ const schedulePermit: PaymentTypes.SchedulePermit = {
feeAddress: '0x4234567890123456789012345678901234567890',
amount: '1000000000000000000', // 1 token
feeAmount: '10000000000000000', // 0.01 token
executorFee: '5000000000000000', // 0.005 token
relayerFee: '5000000000000000', // 0.005 token
periodSeconds: 86400, // 1 day
firstExec: Math.floor(Date.now() / 1000),
totalExecutions: 12,
totalPayments: 12,
nonce: '1',
deadline: Math.floor(Date.now() / 1000) + 3600, // 1 hour from now
strictOrder: true,
Expand Down Expand Up @@ -118,9 +118,9 @@ describe('erc20-recurring-payment-proxy', () => {
});
});

describe('encodeRecurringPaymentExecution', () => {
it('should encode execution data correctly', () => {
const encodedData = encodeRecurringPaymentExecution({
describe('encodeRecurringPaymentTrigger', () => {
it('should encode trigger data correctly', () => {
const encodedData = encodeRecurringPaymentTrigger({
permitTuple: schedulePermit,
permitSignature,
paymentIndex: 1,
Expand All @@ -134,12 +134,12 @@ describe('erc20-recurring-payment-proxy', () => {
});
});

describe('executeRecurringPayment', () => {
describe('triggerRecurringPayment', () => {
it('should throw if proxy not deployed on network', async () => {
jest.spyOn(erc20RecurringPaymentProxyArtifact, 'getAddress').mockReturnValue('');

await expect(
executeRecurringPayment({
triggerRecurringPayment({
permitTuple: schedulePermit,
permitSignature,
paymentIndex: 1,
Expand All @@ -151,3 +151,45 @@ describe('erc20-recurring-payment-proxy', () => {
});
});
});

describe('ERC20 Recurring Payment', () => {
const permit: PaymentTypes.SchedulePermit = {
subscriber: '0x1234567890123456789012345678901234567890',
token: '0x1234567890123456789012345678901234567890',
recipient: '0x1234567890123456789012345678901234567890',
feeAddress: '0x1234567890123456789012345678901234567890',
amount: '1000000000000000000', // 1 token
feeAmount: '10000000000000000', // 0.01 token
relayerFee: '5000000000000000', // 0.005 token
periodSeconds: 86400, // 1 day
firstExec: Math.floor(Date.now() / 1000),
totalPayments: 12,
nonce: 0,
deadline: Math.floor(Date.now() / 1000) + 3600, // 1 hour from now
strictOrder: false,
};

it('should encode recurring payment execution', () => {
const encoded = encodeRecurringPaymentTrigger({
permitTuple: permit,
permitSignature,
paymentIndex: 1,
paymentReference,
network,
provider,
});
expect(encoded).toBeDefined();
});

it('should execute recurring payment', async () => {
const result = await triggerRecurringPayment({
permitTuple: permit,
permitSignature,
paymentIndex: 1,
paymentReference,
signer: wallet,
network,
});
expect(result).toBeDefined();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import './lib/SafeERC20.sol';

/**
* @title ERC20RecurringPaymentProxy
* @notice Executes recurring ERC20 payments.
* @notice Triggers recurring ERC20 payments based on predefined schedules.
*/
contract ERC20RecurringPaymentProxy is EIP712, AccessControl, Pausable, ReentrancyGuard, Ownable {
using SafeERC20 for IERC20;
Expand All @@ -21,26 +21,26 @@ contract ERC20RecurringPaymentProxy is EIP712, AccessControl, Pausable, Reentran
error ERC20RecurringPaymentProxy__BadSignature();
error ERC20RecurringPaymentProxy__SignatureExpired();
error ERC20RecurringPaymentProxy__IndexTooLarge();
error ERC20RecurringPaymentProxy__ExecutionOutOfOrder();
error ERC20RecurringPaymentProxy__PaymentOutOfOrder();
error ERC20RecurringPaymentProxy__IndexOutOfBounds();
error ERC20RecurringPaymentProxy__NotDueYet();
error ERC20RecurringPaymentProxy__AlreadyPaid();
error ERC20RecurringPaymentProxy__ZeroAddress();

bytes32 public constant EXECUTOR_ROLE = keccak256('EXECUTOR_ROLE');
bytes32 public constant RELAYER_ROLE = keccak256('RELAYER_ROLE');

/* keccak256 of the typed-data struct with executorFee field */
/* keccak256 of the typed-data struct with relayerFee field */
bytes32 private constant _PERMIT_TYPEHASH =
keccak256(
'SchedulePermit(address subscriber,address token,address recipient,'
'address feeAddress,uint128 amount,uint128 feeAmount,uint128 executorFee,'
'uint32 periodSeconds,uint32 firstExec,uint8 totalExecutions,'
'address feeAddress,uint128 amount,uint128 feeAmount,uint128 relayerFee,'
'uint32 periodSeconds,uint32 firstExec,uint8 totalPayments,'
'uint256 nonce,uint256 deadline,bool strictOrder)'
);

/* replay defence */
mapping(bytes32 => uint256) public executedBitmap;
mapping(bytes32 => uint8) public lastExecutionIndex;
mapping(bytes32 => uint256) public triggeredPaymentsBitmap;
mapping(bytes32 => uint8) public lastPaymentIndex;

IERC20FeeProxy public erc20FeeProxy;

Expand All @@ -51,27 +51,25 @@ contract ERC20RecurringPaymentProxy is EIP712, AccessControl, Pausable, Reentran
address feeAddress;
uint128 amount;
uint128 feeAmount;
uint128 executorFee;
uint128 relayerFee;
uint32 periodSeconds;
uint32 firstExec;
uint8 totalExecutions;
uint8 totalPayments;
uint256 nonce;
uint256 deadline;
bool strictOrder;
}

constructor(
address adminSafe,
address executorEOA,
address relayerEOA,
address erc20FeeProxyAddress
) EIP712('ERC20RecurringPaymentProxy', '1') {
if (
adminSafe == address(0) || executorEOA == address(0) || erc20FeeProxyAddress == address(0)
) {
if (adminSafe == address(0) || relayerEOA == address(0) || erc20FeeProxyAddress == address(0)) {
revert ERC20RecurringPaymentProxy__ZeroAddress();
}
_grantRole(DEFAULT_ADMIN_ROLE, adminSafe);
_grantRole(EXECUTOR_ROLE, executorEOA);
_grantRole(RELAYER_ROLE, relayerEOA);
transferOwnership(adminSafe);
erc20FeeProxy = IERC20FeeProxy(erc20FeeProxyAddress);
}
Expand All @@ -93,12 +91,12 @@ contract ERC20RecurringPaymentProxy is EIP712, AccessControl, Pausable, Reentran
);
}

function execute(
function triggerRecurringPayment(
SchedulePermit calldata p,
bytes calldata signature,
uint8 index,
bytes calldata paymentReference
) external whenNotPaused onlyRole(EXECUTOR_ROLE) nonReentrant {
) external whenNotPaused onlyRole(RELAYER_ROLE) nonReentrant {
bytes32 digest = _hashSchedule(p);

if (digest.recover(signature) != p.subscriber)
Expand All @@ -108,22 +106,22 @@ contract ERC20RecurringPaymentProxy is EIP712, AccessControl, Pausable, Reentran
if (index >= 256) revert ERC20RecurringPaymentProxy__IndexTooLarge();

if (p.strictOrder) {
if (index != lastExecutionIndex[digest] + 1)
revert ERC20RecurringPaymentProxy__ExecutionOutOfOrder();
lastExecutionIndex[digest] = index;
if (index != lastPaymentIndex[digest] + 1)
revert ERC20RecurringPaymentProxy__PaymentOutOfOrder();
lastPaymentIndex[digest] = index;
}

if (index > p.totalExecutions) revert ERC20RecurringPaymentProxy__IndexOutOfBounds();
if (index > p.totalPayments) revert ERC20RecurringPaymentProxy__IndexOutOfBounds();

uint256 execTime = uint256(p.firstExec) + uint256(index - 1) * p.periodSeconds;
if (block.timestamp < execTime) revert ERC20RecurringPaymentProxy__NotDueYet();

uint256 mask = 1 << index;
uint256 word = executedBitmap[digest];
uint256 word = triggeredPaymentsBitmap[digest];
if (word & mask != 0) revert ERC20RecurringPaymentProxy__AlreadyPaid();
executedBitmap[digest] = word | mask;
triggeredPaymentsBitmap[digest] = word | mask;

uint256 total = p.amount + p.feeAmount + p.executorFee;
uint256 total = p.amount + p.feeAmount + p.relayerFee;

IERC20 token = IERC20(p.token);
token.safeTransferFrom(p.subscriber, address(this), total);
Expand All @@ -134,15 +132,15 @@ contract ERC20RecurringPaymentProxy is EIP712, AccessControl, Pausable, Reentran

_proxyTransfer(p, paymentReference);

if (p.executorFee != 0) {
token.safeTransfer(msg.sender, p.executorFee);
if (p.relayerFee != 0) {
token.safeTransfer(msg.sender, p.relayerFee);
}
}

function setExecutor(address oldExec, address newExec) external onlyOwner {
if (newExec == address(0)) revert ERC20RecurringPaymentProxy__ZeroAddress();
_revokeRole(EXECUTOR_ROLE, oldExec);
_grantRole(EXECUTOR_ROLE, newExec);
function setRelayer(address oldRelayer, address newRelayer) external onlyOwner {
if (newRelayer == address(0)) revert ERC20RecurringPaymentProxy__ZeroAddress();
_revokeRole(RELAYER_ROLE, oldRelayer);
_grantRole(RELAYER_ROLE, newRelayer);
}

function setFeeProxy(address newProxy) external onlyOwner {
Expand Down
Loading