Skip to content
Merged
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
test(ERC20RecurringPaymentProxy): enhance test suite with execution s…
…cenarios and edge case validations
  • Loading branch information
aimensahnoun committed Jun 18, 2025
commit f738b0a602efcfffc1b6cb464e069639d9b1dc55
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,30 @@ describe('ERC20RecurringPaymentProxy', () => {
let user: Signer;
let newExecutor: Signer;
let newOwner: Signer;
let subscriber: Signer;
let recipient: Signer;
let feeAddress: Signer;

let ownerAddress: string;
let executorAddress: string;
let userAddress: string;
let newExecutorAddress: string;
let newOwnerAddress: string;
let subscriberAddress: string;
let recipientAddress: string;
let feeAddressString: string;

beforeEach(async () => {
[owner, executor, user, newExecutor, newOwner] = await ethers.getSigners();
[owner, executor, user, newExecutor, newOwner, subscriber, recipient, feeAddress] =
await ethers.getSigners();
ownerAddress = await owner.getAddress();
executorAddress = await executor.getAddress();
userAddress = await user.getAddress();
newExecutorAddress = await newExecutor.getAddress();
newOwnerAddress = await newOwner.getAddress();
subscriberAddress = await subscriber.getAddress();
recipientAddress = await recipient.getAddress();
feeAddressString = await feeAddress.getAddress();

// Deploy ERC20FeeProxy
const ERC20FeeProxyFactory = await ethers.getContractFactory('ERC20FeeProxy');
Expand All @@ -50,6 +60,56 @@ describe('ERC20RecurringPaymentProxy', () => {
await testERC20.deployed();
});

// Helper function to create a valid SchedulePermit
const createSchedulePermit = (overrides: any = {}) => {
const now = Math.floor(Date.now() / 1000);
return {
subscriber: subscriberAddress,
token: testERC20.address,
recipient: recipientAddress,
feeAddress: feeAddressString,
amount: 100,
feeAmount: 10,
gasFee: 5,
periodSeconds: 3600,
firstExec: now,
totalExecutions: 3,
nonce: 0,
deadline: now + 86400, // 24 hours from now
...overrides,
};
};

// Helper function to create EIP712 signature
const createSignature = async (permit: any, signer: Signer) => {
const domain = {
name: 'ERC20RecurringPaymentProxy',
version: '1',
chainId: await signer.getChainId(),
verifyingContract: erc20RecurringPaymentProxy.address,
};

const types = {
SchedulePermit: [
{ name: 'subscriber', type: 'address' },
{ name: 'token', type: 'address' },
{ name: 'recipient', type: 'address' },
{ name: 'feeAddress', type: 'address' },
{ name: 'amount', type: 'uint128' },
{ name: 'feeAmount', type: 'uint128' },
{ name: 'gasFee', type: 'uint128' },
{ name: 'periodSeconds', type: 'uint32' },
{ name: 'firstExec', type: 'uint32' },
{ name: 'totalExecutions', type: 'uint8' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
],
};

const signature = await (signer as any)._signTypedData(domain, types, permit);
return signature;
};

describe('Deployment', () => {
it('should be deployed with correct initial values', async () => {
expect(erc20RecurringPaymentProxy.address).to.not.equal(ethers.constants.AddressZero);
Expand Down Expand Up @@ -243,6 +303,278 @@ describe('ERC20RecurringPaymentProxy', () => {
});
});

describe('Execute Function', () => {
beforeEach(async () => {
// Transfer tokens to subscriber and approve the recurring payment proxy
await testERC20.transfer(subscriberAddress, 500);
await testERC20.connect(subscriber).approve(erc20RecurringPaymentProxy.address, 500);
});

it('should execute a valid recurring payment', async () => {
const permit = createSchedulePermit();
const signature = await createSignature(permit, subscriber);
const paymentReference = '0x1234567890abcdef';

const subscriberBalanceBefore = await testERC20.balanceOf(subscriberAddress);
const recipientBalanceBefore = await testERC20.balanceOf(recipientAddress);
const feeAddressBalanceBefore = await testERC20.balanceOf(feeAddressString);
const executorBalanceBefore = await testERC20.balanceOf(executorAddress);

await expect(
erc20RecurringPaymentProxy
.connect(executor)
.execute(permit, signature, 1, paymentReference),
)
.to.emit(erc20FeeProxy, 'TransferWithReferenceAndFee')
.withArgs(
testERC20.address,
recipientAddress,
permit.amount,
ethers.utils.keccak256(paymentReference),
permit.feeAmount,
feeAddressString,
);

// Check balance changes
const subscriberBalanceAfter = await testERC20.balanceOf(subscriberAddress);
const recipientBalanceAfter = await testERC20.balanceOf(recipientAddress);
const feeAddressBalanceAfter = await testERC20.balanceOf(feeAddressString);
const executorBalanceAfter = await testERC20.balanceOf(executorAddress);

expect(subscriberBalanceAfter).to.equal(subscriberBalanceBefore.sub(115)); // amount + fee + gas
expect(recipientBalanceAfter).to.equal(recipientBalanceBefore.add(100)); // amount
expect(feeAddressBalanceAfter).to.equal(feeAddressBalanceBefore.add(10)); // fee
expect(executorBalanceAfter).to.equal(executorBalanceBefore.add(5)); // gas fee
});

it('should revert when called by non-executor', async () => {
const permit = createSchedulePermit();
const signature = await createSignature(permit, subscriber);
const paymentReference = '0x1234567890abcdef';

await expect(
erc20RecurringPaymentProxy.connect(user).execute(permit, signature, 1, paymentReference),
).to.be.revertedWith('AccessControl: account');
});

it('should revert when contract is paused', async () => {
await erc20RecurringPaymentProxy.pause();

const permit = createSchedulePermit();
const signature = await createSignature(permit, subscriber);
const paymentReference = '0x1234567890abcdef';

await expect(
erc20RecurringPaymentProxy
.connect(executor)
.execute(permit, signature, 1, paymentReference),
).to.be.revertedWith('Pausable: paused');
});

it('should revert with bad signature', async () => {
const permit = createSchedulePermit();
const signature = await createSignature(permit, user); // Wrong signer
const paymentReference = '0x1234567890abcdef';

await expect(
erc20RecurringPaymentProxy
.connect(executor)
.execute(permit, signature, 1, paymentReference),
).to.be.reverted;
});

it('should revert when signature is expired', async () => {
const permit = createSchedulePermit({
deadline: Math.floor(Date.now() / 1000) - 3600, // 1 hour ago
});
const signature = await createSignature(permit, subscriber);
const paymentReference = '0x1234567890abcdef';

await expect(
erc20RecurringPaymentProxy
.connect(executor)
.execute(permit, signature, 1, paymentReference),
).to.be.reverted;
});

it('should revert when index is too large (>= 256)', async () => {
const permit = createSchedulePermit();
const signature = await createSignature(permit, subscriber);
const paymentReference = '0x1234567890abcdef';

await expect(
erc20RecurringPaymentProxy
.connect(executor)
.execute(permit, signature, 256, paymentReference),
).to.be.reverted;
});

it('should revert when execution is out of order', async () => {
const permit = createSchedulePermit();
const signature = await createSignature(permit, subscriber);
const paymentReference = '0x1234567890abcdef';

// Try to execute index 2 before index 1
await expect(
erc20RecurringPaymentProxy
.connect(executor)
.execute(permit, signature, 2, paymentReference),
).to.be.reverted;
});

it('should revert when index is out of bounds', async () => {
const permit = createSchedulePermit({ totalExecutions: 1 });
const signature = await createSignature(permit, subscriber);
const paymentReference = '0x1234567890abcdef';

await expect(
erc20RecurringPaymentProxy
.connect(executor)
.execute(permit, signature, 2, paymentReference),
).to.be.reverted;
});

it('should revert when payment is not due yet', async () => {
const permit = createSchedulePermit({
firstExec: Math.floor(Date.now() / 1000) + 3600, // 1 hour from now
});
const signature = await createSignature(permit, subscriber);
const paymentReference = '0x1234567890abcdef';

await expect(
erc20RecurringPaymentProxy
.connect(executor)
.execute(permit, signature, 1, paymentReference),
).to.be.reverted;
});

it('should revert when payment is already executed', async () => {
const permit = createSchedulePermit();
const signature = await createSignature(permit, subscriber);
const paymentReference = '0x1234567890abcdef';

// Execute first time
await erc20RecurringPaymentProxy
.connect(executor)
.execute(permit, signature, 1, paymentReference);

// Try to execute the same index again
await expect(
erc20RecurringPaymentProxy
.connect(executor)
.execute(permit, signature, 1, paymentReference),
).to.be.reverted;
});

it('should allow sequential execution of multiple payments', async () => {
const permit = createSchedulePermit({ totalExecutions: 3 });
const signature = await createSignature(permit, subscriber);
const paymentReference = '0x1234567890abcdef';

// Execute first payment
await erc20RecurringPaymentProxy
.connect(executor)
.execute(permit, signature, 1, paymentReference);

// Advance time by periodSeconds to allow second payment
await ethers.provider.send('evm_increaseTime', [permit.periodSeconds]);
await ethers.provider.send('evm_mine', []);

// Execute second payment
await erc20RecurringPaymentProxy
.connect(executor)
.execute(permit, signature, 2, paymentReference);

// Advance time by periodSeconds to allow third payment
await ethers.provider.send('evm_increaseTime', [permit.periodSeconds]);
await ethers.provider.send('evm_mine', []);

// Execute third payment
await erc20RecurringPaymentProxy
.connect(executor)
.execute(permit, signature, 3, paymentReference);

// Verify all payments were executed
// Note: We can't directly call _hashSchedule as it's private, but we can verify through the bitmap
// The bitmap should have bits 1, 2, and 3 set (2^1 + 2^2 + 2^3 = 14)
// We'll check this by trying to execute the same indices again, which should fail
await expect(
erc20RecurringPaymentProxy
.connect(executor)
.execute(permit, signature, 1, paymentReference),
).to.be.reverted; // Should fail because already executed

await expect(
erc20RecurringPaymentProxy
.connect(executor)
.execute(permit, signature, 2, paymentReference),
).to.be.reverted; // Should fail because already executed

await expect(
erc20RecurringPaymentProxy
.connect(executor)
.execute(permit, signature, 3, paymentReference),
).to.be.reverted; // Should fail because already executed
});

it('should handle zero gas fee correctly', async () => {
const permit = createSchedulePermit({ gasFee: 0 });
const signature = await createSignature(permit, subscriber);
const paymentReference = '0x1234567890abcdef';

const executorBalanceBefore = await testERC20.balanceOf(executorAddress);

await erc20RecurringPaymentProxy
.connect(executor)
.execute(permit, signature, 1, paymentReference);

const executorBalanceAfter = await testERC20.balanceOf(executorAddress);
expect(executorBalanceAfter).to.equal(executorBalanceBefore); // No gas fee transferred
});

it('should handle zero fee amount correctly', async () => {
const permit = createSchedulePermit({ feeAmount: 0 });
const signature = await createSignature(permit, subscriber);
const paymentReference = '0x1234567890abcdef';

const feeAddressBalanceBefore = await testERC20.balanceOf(feeAddressString);

await erc20RecurringPaymentProxy
.connect(executor)
.execute(permit, signature, 1, paymentReference);

const feeAddressBalanceAfter = await testERC20.balanceOf(feeAddressString);
expect(feeAddressBalanceAfter).to.equal(feeAddressBalanceBefore); // No fee transferred
});

it('should revert when subscriber has insufficient balance', async () => {
const permit = createSchedulePermit({ amount: 1000 }); // More than subscriber has
const signature = await createSignature(permit, subscriber);
const paymentReference = '0x1234567890abcdef';

await expect(
erc20RecurringPaymentProxy
.connect(executor)
.execute(permit, signature, 1, paymentReference),
).to.be.reverted;
});

it('should revert when subscriber has insufficient allowance', async () => {
const permit = createSchedulePermit();
const signature = await createSignature(permit, subscriber);
const paymentReference = '0x1234567890abcdef';

// Revoke approval
await testERC20.connect(subscriber).approve(erc20RecurringPaymentProxy.address, 0);

await expect(
erc20RecurringPaymentProxy
.connect(executor)
.execute(permit, signature, 1, paymentReference),
).to.be.reverted;
});
});

describe('Integration: Paused state affects execution', () => {
it('should revert execute when contract is paused', async () => {
await erc20RecurringPaymentProxy.pause();
Expand Down