diff --git a/packages/smart-contracts/src/contracts/ERC20RecurringPaymentProxy.sol b/packages/smart-contracts/src/contracts/ERC20RecurringPaymentProxy.sol index a24f10463..a84305baf 100644 --- a/packages/smart-contracts/src/contracts/ERC20RecurringPaymentProxy.sol +++ b/packages/smart-contracts/src/contracts/ERC20RecurringPaymentProxy.sol @@ -26,6 +26,7 @@ contract ERC20RecurringPaymentProxy is EIP712, AccessControl, Pausable, Reentran error ERC20RecurringPaymentProxy__ZeroAddress(); error ERC20RecurringPaymentProxy__TransferFailed(); error ERC20RecurringPaymentProxy__ShortPull(); + error ERC20RecurringPaymentProxy__UnexpectedBalance(); error ERC20RecurringPaymentProxy__ZeroScheduleId(); error ERC20RecurringPaymentProxy__InvalidDueTimes(); error ERC20RecurringPaymentProxy__TooManyLegs(); @@ -39,15 +40,6 @@ contract ERC20RecurringPaymentProxy is EIP712, AccessControl, Pausable, Reentran bytes32 public constant RELAYER_ROLE = keccak256('RELAYER_ROLE'); - /* 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 relayerFee,' - 'uint32 periodSeconds,uint32 firstPayment,uint8 totalPayments,' - 'uint256 nonce,uint256 deadline,bool strictOrder)' - ); - bytes32 private constant _LEG_TYPEHASH = keccak256('Leg(address recipient,uint128 amount,bytes8 paymentReference)'); @@ -68,22 +60,6 @@ contract ERC20RecurringPaymentProxy is EIP712, AccessControl, Pausable, Reentran IERC20FeeProxy public erc20FeeProxy; - struct SchedulePermit { - address subscriber; - address token; - address recipient; - address feeAddress; - uint128 amount; - uint128 feeAmount; - uint128 relayerFee; - uint32 periodSeconds; - uint32 firstPayment; - uint8 totalPayments; - uint256 nonce; - uint256 deadline; - bool strictOrder; - } - struct Leg { address recipient; uint128 amount; @@ -118,16 +94,6 @@ contract ERC20RecurringPaymentProxy is EIP712, AccessControl, Pausable, Reentran erc20FeeProxy = IERC20FeeProxy(erc20FeeProxyAddress); } - function _hashSchedule(SchedulePermit calldata p) private view returns (bytes32) { - bytes32 structHash = keccak256(abi.encode(_PERMIT_TYPEHASH, p)); - - return _hashTypedDataV4(structHash); - } - - function hashSchedule(SchedulePermit calldata p) public view returns (bytes32) { - return _hashSchedule(p); - } - function _hashUint32Array(uint32[] calldata values) private pure returns (bytes32) { bytes32[] memory words = new bytes32[](values.length); for (uint256 i = 0; i < values.length; ++i) { @@ -183,29 +149,6 @@ contract ERC20RecurringPaymentProxy is EIP712, AccessControl, Pausable, Reentran } } - function _scheduleKeyFromPermit(SchedulePermit calldata p) private pure returns (bytes32) { - return - keccak256( - abi.encode( - p.subscriber, - p.token, - p.recipient, - p.feeAddress, - p.amount, - p.feeAmount, - p.relayerFee, - p.periodSeconds, - p.firstPayment, - p.totalPayments, - p.strictOrder - ) - ); - } - - function scheduleKeyFromPermit(SchedulePermit calldata p) public pure returns (bytes32) { - return _scheduleKeyFromPermit(p); - } - function _scheduleKeyFromBatch(SchedulePermitBatch calldata p) private pure returns (bytes32) { if (p.scheduleId == bytes32(0)) revert ERC20RecurringPaymentProxy__ZeroScheduleId(); return @@ -297,8 +240,8 @@ contract ERC20RecurringPaymentProxy is EIP712, AccessControl, Pausable, Reentran IERC20 token, address from, uint256 amount - ) private { - uint256 balanceBefore = token.balanceOf(address(this)); + ) private returns (uint256 balanceBefore) { + balanceBefore = token.balanceOf(address(this)); if (!token.safeTransferFrom(from, address(this), amount)) { revert ERC20RecurringPaymentProxy__TransferFailed(); } @@ -371,51 +314,6 @@ contract ERC20RecurringPaymentProxy is EIP712, AccessControl, Pausable, Reentran } } - function _proxyTransfer(SchedulePermit calldata p, bytes calldata paymentReference) private { - erc20FeeProxy.transferFromWithReferenceAndFee( - p.token, - p.recipient, - p.amount, - paymentReference, - p.feeAmount, - p.feeAddress - ); - } - - function triggerRecurringPayment( - SchedulePermit calldata p, - bytes calldata signature, - uint8 index, - bytes calldata paymentReference - ) external whenNotPaused onlyRole(RELAYER_ROLE) nonReentrant { - bytes32 digest = _hashSchedule(p); - - _assertSigner(p.subscriber, digest, signature); - if (block.timestamp > p.deadline) revert ERC20RecurringPaymentProxy__SignatureExpired(); - - if (index == 0) revert ERC20RecurringPaymentProxy__IndexOutOfBounds(); - if (index > p.totalPayments) revert ERC20RecurringPaymentProxy__IndexOutOfBounds(); - - bytes32 scheduleKey = _scheduleKeyFromPermit(p); - _assertNotCancelled(scheduleKey); - _assertOrder(scheduleKey, index, p.strictOrder); - _assertUnpaid(scheduleKey, index); - - uint256 execTime = uint256(p.firstPayment) + uint256(index - 1) * p.periodSeconds; - if (block.timestamp < execTime) revert ERC20RecurringPaymentProxy__NotDueYet(); - - _assertNonZeroRecipient(p.feeAddress, p.feeAmount); - - uint256 total = p.amount + p.feeAmount + p.relayerFee; - - IERC20 token = IERC20(p.token); - _pullExact(token, p.subscriber, total); - _approveFeeProxy(token, erc20FeeProxy, p.amount + p.feeAmount); - _proxyTransfer(p, paymentReference); - _payRelayer(token, p.relayerFee); - _markPaid(scheduleKey, index, p.strictOrder); - } - function triggerRecurringPaymentBatch( SchedulePermitBatch calldata p, bytes calldata signature, @@ -465,7 +363,7 @@ contract ERC20RecurringPaymentProxy is EIP712, AccessControl, Pausable, Reentran IERC20 token = IERC20(p.token); IERC20FeeProxy proxy = erc20FeeProxy; - _pullExact(token, p.subscriber, payerTotal); + uint256 baseline = _pullExact(token, p.subscriber, payerTotal); _approveFeeProxy(token, proxy, legsSum); if (useInitial) { _settleLegs(proxy, p.token, p.initialLegs); @@ -473,17 +371,9 @@ contract ERC20RecurringPaymentProxy is EIP712, AccessControl, Pausable, Reentran _settleLegs(proxy, p.token, p.recurringLegs); } _payRelayer(token, p.relayerFee); - } - - /** - * @notice Blocks further triggers for this single-fee schedule. - * @dev Does not revoke the subscriber's ERC-20 allowance to this contract. A relayer can - * still collect a due cycle if they include a trigger in the same block ahead of - * cancel. Also `approve` this proxy to 0 (or decrease) in the same wallet batch. - */ - function cancelSchedule(SchedulePermit calldata p) external { - _assertSubscriber(p.subscriber); - _cancel(_scheduleKeyFromPermit(p)); + if (token.balanceOf(address(this)) != baseline) { + revert ERC20RecurringPaymentProxy__UnexpectedBalance(); + } } /** diff --git a/packages/smart-contracts/test/contracts/ERC20RecurringPaymentProxy.test.ts b/packages/smart-contracts/test/contracts/ERC20RecurringPaymentProxy.test.ts index 067d81dc9..67c4c4e61 100644 --- a/packages/smart-contracts/test/contracts/ERC20RecurringPaymentProxy.test.ts +++ b/packages/smart-contracts/test/contracts/ERC20RecurringPaymentProxy.test.ts @@ -60,45 +60,6 @@ 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, - relayerFee: 5, - periodSeconds: 3600, - firstPayment: now, - totalPayments: 3, - nonce: 0, - deadline: now + 86400, // 24 hours from now - strictOrder: false, - ...overrides, - }; - }; - - const schedulePermitTypes = { - 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: 'relayerFee', type: 'uint128' }, - { name: 'periodSeconds', type: 'uint32' }, - { name: 'firstPayment', type: 'uint32' }, - { name: 'totalPayments', type: 'uint8' }, - { name: 'nonce', type: 'uint256' }, - { name: 'deadline', type: 'uint256' }, - { name: 'strictOrder', type: 'bool' }, - ], - }; - const schedulePermitBatchTypes = { SchedulePermitBatch: [ { name: 'subscriber', type: 'address' }, @@ -127,46 +88,9 @@ describe('ERC20RecurringPaymentProxy', () => { verifyingContract: erc20RecurringPaymentProxy.address, }); - const hashPermitOffchain = async (permit: any) => - ethers.utils._TypedDataEncoder.hash(await eip712Domain(), schedulePermitTypes, permit); - const hashBatchOffchain = async (permit: any) => ethers.utils._TypedDataEncoder.hash(await eip712Domain(), schedulePermitBatchTypes, permit); - // Helper function to create EIP712 signature - const createSignature = async (permit: any, signer: Signer) => { - const domain = await eip712Domain(); - - // Some providers (Hardhat in-process) happily accept the string-encoded data (what - // ethers' _signTypedData sends). Others (Hardhat JSON-RPC, Ganache) expect the object - // version. To work everywhere we try the object version first and fall back to - // the built-in helper if the call is rejected. - - const typedDataObject = { - types: { - EIP712Domain: [ - { name: 'name', type: 'string' }, - { name: 'version', type: 'string' }, - { name: 'chainId', type: 'uint256' }, - { name: 'verifyingContract', type: 'address' }, - ], - ...schedulePermitTypes, - }, - primaryType: 'SchedulePermit', - domain, - message: permit, - }; - - const address = await signer.getAddress(); - try { - // This matches the spec used by Hardhat JSON-RPC & Ganache - return await (signer.provider as any).send('eth_signTypedData', [address, typedDataObject]); - } catch (_) { - // Fallback to ethers helper (works in most in-process Hardhat environments) - return await (signer as any)._signTypedData(domain, schedulePermitTypes, permit); - } - }; - const createBatchSignature = async (permit: any, signer: Signer) => { const domain = await eip712Domain(); const address = await signer.getAddress(); @@ -381,24 +305,6 @@ describe('ERC20RecurringPaymentProxy', () => { }); describe('Fee destination and rescue', () => { - const paymentReference = '0x1234567890abcdef'; - - it('reverts when feeAmount is non-zero and feeAddress is zero', async () => { - await testERC20.transfer(subscriberAddress, 500); - await testERC20.connect(subscriber).approve(erc20RecurringPaymentProxy.address, 500); - - const permit = createSchedulePermit({ feeAddress: ethers.constants.AddressZero }); - const signature = await createSignature(permit, subscriber); - const scheduleKey = await erc20RecurringPaymentProxy.scheduleKeyFromPermit(permit); - - await expect( - erc20RecurringPaymentProxy - .connect(relayer) - .triggerRecurringPayment(permit, signature, 1, paymentReference), - ).to.be.revertedWith('ERC20RecurringPaymentProxy__ZeroAddress'); - expect(await erc20RecurringPaymentProxy.triggeredPaymentsBitmap(scheduleKey)).to.equal(0); - }); - it('allows the owner to rescue a residual balance', async () => { await testERC20.transfer(erc20RecurringPaymentProxy.address, 40); const ownerBalanceBefore = await testERC20.balanceOf(ownerAddress); @@ -430,449 +336,7 @@ describe('ERC20RecurringPaymentProxy', () => { }); }); - describe('Trigger Recurring Payment', () => { - 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 trigger 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 relayerBalanceBefore = await testERC20.balanceOf(relayerAddress); - - await expect( - erc20RecurringPaymentProxy - .connect(relayer) - .triggerRecurringPayment(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 relayerBalanceAfter = await testERC20.balanceOf(relayerAddress); - - 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(relayerBalanceAfter).to.equal(relayerBalanceBefore.add(5)); // gas fee - }); - - it('should revert when called by non-relayer', async () => { - const permit = createSchedulePermit(); - const signature = await createSignature(permit, subscriber); - const paymentReference = '0x1234567890abcdef'; - - await expect( - erc20RecurringPaymentProxy - .connect(user) - .triggerRecurringPayment(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(relayer) - .triggerRecurringPayment(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(relayer) - .triggerRecurringPayment(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(relayer) - .triggerRecurringPayment(permit, signature, 1, paymentReference), - ).to.be.reverted; - }); - - it('should revert when execution is out of order', async () => { - const permit = createSchedulePermit({ strictOrder: true, periodSeconds: 1 }); - const signature = await createSignature(permit, subscriber); - const paymentReference = '0x1234567890abcdef'; - - // Advance time so payment #2 is due, ensuring the only failure reason is order. - await ethers.provider.send('evm_increaseTime', [1]); - await ethers.provider.send('evm_mine', []); - - // Try to execute index 2 before index 1 - await expect( - erc20RecurringPaymentProxy - .connect(relayer) - .triggerRecurringPayment(permit, signature, 2, paymentReference), - ).to.be.reverted; - }); - - it('should allow out of order trigger if strictOrder is false', async () => { - const permit = createSchedulePermit({ strictOrder: false, periodSeconds: 1 }); - const signature = await createSignature(permit, subscriber); - const paymentReference = '0x1234567890abcdef'; - - // Fast forward time to make multiple payments due - await ethers.provider.send('evm_increaseTime', [5]); - await ethers.provider.send('evm_mine', []); - - // Execute index 2 before index 1, which should be allowed - await expect( - erc20RecurringPaymentProxy - .connect(relayer) - .triggerRecurringPayment(permit, signature, 2, paymentReference), - ).to.not.be.reverted; - }); - - it('should revert when index is out of bounds', async () => { - const permit = createSchedulePermit({ totalPayments: 1 }); - const signature = await createSignature(permit, subscriber); - const paymentReference = '0x1234567890abcdef'; - - await expect( - erc20RecurringPaymentProxy - .connect(relayer) - .triggerRecurringPayment(permit, signature, 2, paymentReference), - ).to.be.reverted; - }); - - it('should revert when payment is not due yet', async () => { - const permit = createSchedulePermit({ - firstPayment: Math.floor(Date.now() / 1000) + 3600, // 1 hour from now - }); - const signature = await createSignature(permit, subscriber); - const paymentReference = '0x1234567890abcdef'; - - await expect( - erc20RecurringPaymentProxy - .connect(relayer) - .triggerRecurringPayment(permit, signature, 1, paymentReference), - ).to.be.reverted; - }); - - it('should revert when payment is already triggered', async () => { - const permit = createSchedulePermit(); - const signature = await createSignature(permit, subscriber); - const paymentReference = '0x1234567890abcdef'; - - // Trigger first time - await erc20RecurringPaymentProxy - .connect(relayer) - .triggerRecurringPayment(permit, signature, 1, paymentReference); - - // Try to trigger the same index again - await expect( - erc20RecurringPaymentProxy - .connect(relayer) - .triggerRecurringPayment(permit, signature, 1, paymentReference), - ).to.be.reverted; - }); - - it('should allow sequential trigger of multiple payments', async () => { - const permit = createSchedulePermit({ totalPayments: 3, periodSeconds: 1 }); - const signature = await createSignature(permit, subscriber); - const paymentReference = '0x1234567890abcdef'; - - // Trigger first payment - await erc20RecurringPaymentProxy - .connect(relayer) - .triggerRecurringPayment(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', []); - - // Trigger second payment - await erc20RecurringPaymentProxy - .connect(relayer) - .triggerRecurringPayment(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', []); - - // Trigger third payment - await erc20RecurringPaymentProxy - .connect(relayer) - .triggerRecurringPayment(permit, signature, 3, paymentReference); - - // Verify all payments were triggered - // 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 trigger the same indices again, which should fail - await expect( - erc20RecurringPaymentProxy - .connect(relayer) - .triggerRecurringPayment(permit, signature, 1, paymentReference), - ).to.be.reverted; // Should fail because already triggered - - await expect( - erc20RecurringPaymentProxy - .connect(relayer) - .triggerRecurringPayment(permit, signature, 2, paymentReference), - ).to.be.reverted; // Should fail because already triggered - - await expect( - erc20RecurringPaymentProxy - .connect(relayer) - .triggerRecurringPayment(permit, signature, 3, paymentReference), - ).to.be.reverted; // Should fail because already triggered - }); - - it('should handle zero relayer fee correctly', async () => { - const permit = createSchedulePermit({ relayerFee: 0 }); - const signature = await createSignature(permit, subscriber); - const paymentReference = '0x1234567890abcdef'; - - const relayerBalanceBefore = await testERC20.balanceOf(relayerAddress); - - await erc20RecurringPaymentProxy - .connect(relayer) - .triggerRecurringPayment(permit, signature, 1, paymentReference); - - const relayerBalanceAfter = await testERC20.balanceOf(relayerAddress); - expect(relayerBalanceAfter).to.equal(relayerBalanceBefore); // No relayer 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(relayer) - .triggerRecurringPayment(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(relayer) - .triggerRecurringPayment(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(relayer) - .triggerRecurringPayment(permit, signature, 1, paymentReference), - ).to.be.reverted; - }); - }); - - describe('Pull assertions', () => { - const paymentReference = '0x1234567890abcdef'; - - it('reverts an under-funded pull, leaves the bitmap unset, and stays collectable after funding', async () => { - await testERC20.transfer(subscriberAddress, 50); - await testERC20.connect(subscriber).approve(erc20RecurringPaymentProxy.address, 500); - - const permit = createSchedulePermit(); - const signature = await createSignature(permit, subscriber); - const scheduleKey = await erc20RecurringPaymentProxy.scheduleKeyFromPermit(permit); - - await expect( - erc20RecurringPaymentProxy - .connect(relayer) - .triggerRecurringPayment(permit, signature, 1, paymentReference), - ).to.be.reverted; - expect(await erc20RecurringPaymentProxy.triggeredPaymentsBitmap(scheduleKey)).to.equal(0); - - await testERC20.transfer(subscriberAddress, 500); - await erc20RecurringPaymentProxy - .connect(relayer) - .triggerRecurringPayment(permit, signature, 1, paymentReference); - expect(await erc20RecurringPaymentProxy.triggeredPaymentsBitmap(scheduleKey)).to.not.equal(0); - }); - - it('cannot settle an unfunded subscriber from a residual proxy balance', async () => { - const SilentFailFactory = await ethers.getContractFactory('ERC20SilentFail'); - const silentFail = await SilentFailFactory.deploy(1000); - await silentFail.deployed(); - - await silentFail.transfer(erc20RecurringPaymentProxy.address, 500); - - const permit = createSchedulePermit({ token: silentFail.address }); - const signature = await createSignature(permit, subscriber); - const scheduleKey = await erc20RecurringPaymentProxy.scheduleKeyFromPermit(permit); - - await expect( - erc20RecurringPaymentProxy - .connect(relayer) - .triggerRecurringPayment(permit, signature, 1, paymentReference), - ).to.be.revertedWith('ERC20RecurringPaymentProxy__TransferFailed'); - expect(await erc20RecurringPaymentProxy.triggeredPaymentsBitmap(scheduleKey)).to.equal(0); - expect(await silentFail.balanceOf(erc20RecurringPaymentProxy.address)).to.equal(500); - expect(await silentFail.balanceOf(recipientAddress)).to.equal(0); - }); - - it('reverts a fee-on-transfer token that under-delivers', async () => { - const FeeOnTransferFactory = await ethers.getContractFactory('ERC20FeeOnTransfer'); - const feeOnTransfer = await FeeOnTransferFactory.deploy(1000); - await feeOnTransfer.deployed(); - - await feeOnTransfer.transfer(subscriberAddress, 500); - await feeOnTransfer.connect(subscriber).approve(erc20RecurringPaymentProxy.address, 500); - - const permit = createSchedulePermit({ token: feeOnTransfer.address }); - const signature = await createSignature(permit, subscriber); - const scheduleKey = await erc20RecurringPaymentProxy.scheduleKeyFromPermit(permit); - - await expect( - erc20RecurringPaymentProxy - .connect(relayer) - .triggerRecurringPayment(permit, signature, 1, paymentReference), - ).to.be.revertedWith('ERC20RecurringPaymentProxy__ShortPull'); - expect(await erc20RecurringPaymentProxy.triggeredPaymentsBitmap(scheduleKey)).to.equal(0); - }); - - it('reverts when the token returns false without reverting', async () => { - const SilentFailFactory = await ethers.getContractFactory('ERC20SilentFail'); - const silentFail = await SilentFailFactory.deploy(1000); - await silentFail.deployed(); - - await silentFail.transfer(subscriberAddress, 500); - // No approve: transferFrom returns false instead of reverting. - - const permit = createSchedulePermit({ token: silentFail.address }); - const signature = await createSignature(permit, subscriber); - const scheduleKey = await erc20RecurringPaymentProxy.scheduleKeyFromPermit(permit); - - await expect( - erc20RecurringPaymentProxy - .connect(relayer) - .triggerRecurringPayment(permit, signature, 1, paymentReference), - ).to.be.revertedWith('ERC20RecurringPaymentProxy__TransferFailed'); - expect(await erc20RecurringPaymentProxy.triggeredPaymentsBitmap(scheduleKey)).to.equal(0); - }); - - it('does not mark the cycle paid when the relayer-fee transfer fails', async () => { - const FailTransferFactory = await ethers.getContractFactory('ERC20FailTransfer'); - const failTransfer = await FailTransferFactory.deploy(1000); - await failTransfer.deployed(); - - await failTransfer.transfer(subscriberAddress, 500); - await failTransfer.connect(subscriber).approve(erc20RecurringPaymentProxy.address, 500); - - const permit = createSchedulePermit({ token: failTransfer.address }); - const signature = await createSignature(permit, subscriber); - const scheduleKey = await erc20RecurringPaymentProxy.scheduleKeyFromPermit(permit); - - await expect( - erc20RecurringPaymentProxy - .connect(relayer) - .triggerRecurringPayment(permit, signature, 1, paymentReference), - ).to.be.revertedWith('ERC20RecurringPaymentProxy__TransferFailed'); - expect(await erc20RecurringPaymentProxy.triggeredPaymentsBitmap(scheduleKey)).to.equal(0); - expect(await failTransfer.balanceOf(recipientAddress)).to.equal(0); - }); - }); - describe('Schedule key replay', () => { - const paymentReference = '0x1234567890abcdef'; - - it('re-signing with a new nonce or deadline does not reset paid indices', async () => { - await testERC20.transfer(subscriberAddress, 500); - await testERC20.connect(subscriber).approve(erc20RecurringPaymentProxy.address, 500); - - const permit = createSchedulePermit(); - const signature = await createSignature(permit, subscriber); - await erc20RecurringPaymentProxy - .connect(relayer) - .triggerRecurringPayment(permit, signature, 1, paymentReference); - - const resigned = { ...permit, nonce: 1, deadline: permit.deadline + 86400 }; - const resignedSignature = await createSignature(resigned, subscriber); - const scheduleKey = await erc20RecurringPaymentProxy.scheduleKeyFromPermit(permit); - - expect(await erc20RecurringPaymentProxy.scheduleKeyFromPermit(resigned)).to.equal( - scheduleKey, - ); - await expect( - erc20RecurringPaymentProxy - .connect(relayer) - .triggerRecurringPayment(resigned, resignedSignature, 1, paymentReference), - ).to.be.revertedWith('ERC20RecurringPaymentProxy__AlreadyPaid'); - }); - - it('rejects index 0', async () => { - await testERC20.transfer(subscriberAddress, 500); - await testERC20.connect(subscriber).approve(erc20RecurringPaymentProxy.address, 500); - - const permit = createSchedulePermit(); - const signature = await createSignature(permit, subscriber); - - await expect( - erc20RecurringPaymentProxy - .connect(relayer) - .triggerRecurringPayment(permit, signature, 0, paymentReference), - ).to.be.revertedWith('ERC20RecurringPaymentProxy__IndexOutOfBounds'); - }); - - it('rejects index 256 before the call is encoded', async () => { - const permit = createSchedulePermit(); - const signature = await createSignature(permit, subscriber); - - await expect( - erc20RecurringPaymentProxy - .connect(relayer) - .triggerRecurringPayment(permit, signature, 256, paymentReference), - ).to.be.reverted; - }); - it('keeps the batch schedule key stable across nonce and deadline re-sign', async () => { const permit = { subscriber: subscriberAddress, @@ -919,133 +383,35 @@ describe('ERC20RecurringPaymentProxy', () => { ).to.not.equal(key); expect( await erc20RecurringPaymentProxy.scheduleKeyFromBatch({ - ...permit, - recurringLegs: [ - { - recipient: recipientAddress, - amount: 1, - paymentReference: ethers.utils.hexZeroPad('0x01', 8), - }, - ], - }), - ).to.not.equal(key); - }); - - it('rejects a zero batch scheduleId', async () => { - const permit = { - subscriber: subscriberAddress, - token: testERC20.address, - relayerFee: 0, - totalPayments: 1, - nonce: 0, - deadline: Math.floor(Date.now() / 1000) + 86400, - strictOrder: false, - scheduleId: ethers.constants.HashZero, - dueTimes: [Math.floor(Date.now() / 1000)], - initialLegs: [], - recurringLegs: [], - }; - await expect(erc20RecurringPaymentProxy.scheduleKeyFromBatch(permit)).to.be.revertedWith( - 'ERC20RecurringPaymentProxy__ZeroScheduleId', - ); - }); - }); - - describe('EIP-1271 signatures', () => { - const paymentReference = '0x1234567890abcdef'; - - it('accepts a valid smart-account signature', async () => { - const MockERC1271Factory = await ethers.getContractFactory('MockERC1271'); - const mockWallet = await MockERC1271Factory.deploy(subscriberAddress); - await mockWallet.deployed(); - - await testERC20.transfer(mockWallet.address, 500); - await mockWallet - .connect(subscriber) - .approveToken(testERC20.address, erc20RecurringPaymentProxy.address, 500); - - const permit = createSchedulePermit({ subscriber: mockWallet.address }); - const signature = await createSignature(permit, subscriber); - - await expect( - erc20RecurringPaymentProxy - .connect(relayer) - .triggerRecurringPayment(permit, signature, 1, paymentReference), - ) - .to.emit(erc20FeeProxy, 'TransferWithReferenceAndFee') - .withArgs( - testERC20.address, - recipientAddress, - permit.amount, - ethers.utils.keccak256(paymentReference), - permit.feeAmount, - feeAddressString, - ); - }); - - it('rejects a malformed smart-account signature', async () => { - const MockERC1271Factory = await ethers.getContractFactory('MockERC1271'); - const mockWallet = await MockERC1271Factory.deploy(subscriberAddress); - await mockWallet.deployed(); - - await testERC20.transfer(mockWallet.address, 500); - await mockWallet - .connect(subscriber) - .approveToken(testERC20.address, erc20RecurringPaymentProxy.address, 500); - - const permit = createSchedulePermit({ subscriber: mockWallet.address }); - const signature = '0x' + '11'.repeat(65); - - await expect( - erc20RecurringPaymentProxy - .connect(relayer) - .triggerRecurringPayment(permit, signature, 1, paymentReference), - ).to.be.reverted; - }); - - it('still accepts an EOA signature through SignatureChecker', async () => { - await testERC20.transfer(subscriberAddress, 500); - await testERC20.connect(subscriber).approve(erc20RecurringPaymentProxy.address, 500); - - const permit = createSchedulePermit(); - const signature = await createSignature(permit, subscriber); - - await expect( - erc20RecurringPaymentProxy - .connect(relayer) - .triggerRecurringPayment(permit, signature, 1, paymentReference), - ).to.emit(erc20FeeProxy, 'TransferWithReferenceAndFee'); + ...permit, + recurringLegs: [ + { + recipient: recipientAddress, + amount: 1, + paymentReference: ethers.utils.hexZeroPad('0x01', 8), + }, + ], + }), + ).to.not.equal(key); }); - }); - - describe('Integration: Paused state affects execution', () => { - it('should revert trigger when contract is paused', async () => { - await erc20RecurringPaymentProxy.pause(); - // Create a minimal SchedulePermit for testing - const schedulePermit = { - subscriber: userAddress, + it('rejects a zero batch scheduleId', async () => { + const permit = { + subscriber: subscriberAddress, token: testERC20.address, - recipient: userAddress, - feeAddress: userAddress, - amount: 100, - feeAmount: 10, - relayerFee: 5, - periodSeconds: 3600, - firstPayment: Math.floor(Date.now() / 1000), + relayerFee: 0, totalPayments: 1, nonce: 0, - deadline: Math.floor(Date.now() / 1000) + 3600, + deadline: Math.floor(Date.now() / 1000) + 86400, + strictOrder: false, + scheduleId: ethers.constants.HashZero, + dueTimes: [Math.floor(Date.now() / 1000)], + initialLegs: [], + recurringLegs: [], }; - - const signature = '0x' + '0'.repeat(130); // Dummy signature - const paymentReference = '0x1234'; - - await expect( - erc20RecurringPaymentProxy - .connect(relayer) - .triggerRecurringPayment(schedulePermit, signature, 1, paymentReference), - ).to.be.revertedWith('Pausable: paused'); + await expect(erc20RecurringPaymentProxy.scheduleKeyFromBatch(permit)).to.be.revertedWith( + 'ERC20RecurringPaymentProxy__ZeroScheduleId', + ); }); }); @@ -1152,21 +518,6 @@ describe('ERC20RecurringPaymentProxy', () => { expect(await token.balanceOf(recipientAddress)).to.equal(129_000_000); }); - it('still collects a pre-existing single-fee permit on the same instance', async () => { - await testERC20.transfer(subscriberAddress, 500); - await testERC20.connect(subscriber).approve(erc20RecurringPaymentProxy.address, 500); - - const now = (await ethers.provider.getBlock('latest')).timestamp; - const permit = createSchedulePermit({ firstPayment: now, deadline: now + 86400 }); - const signature = await createSignature(permit, subscriber); - - await expect( - erc20RecurringPaymentProxy - .connect(relayer) - .triggerRecurringPayment(permit, signature, 1, '0x1234567890abcdef'), - ).to.emit(erc20FeeProxy, 'TransferWithReferenceAndFee'); - }); - it('reverts a zero token without moving balances', async () => { await testERC20.transfer(subscriberAddress, 500); await testERC20.connect(subscriber).approve(erc20RecurringPaymentProxy.address, 500); @@ -1360,103 +711,249 @@ describe('ERC20RecurringPaymentProxy', () => { }); }); - describe('cancelSchedule', () => { - const paymentReference = '0x1234567890abcdef'; + describe('Pull assertions', () => { const ref = (n: number) => ethers.utils.hexZeroPad(ethers.utils.hexlify(n), 8); - const latestTs = async () => (await ethers.provider.getBlock('latest')).timestamp; - - const simpleBatch = async () => { - const now = await latestTs(); + const pullPermit = async (tokenAddress: string, overrides: Record = {}) => { + const now = (await ethers.provider.getBlock('latest')).timestamp; return { subscriber: subscriberAddress, - token: testERC20.address, - relayerFee: 0, + token: tokenAddress, + relayerFee: 5, totalPayments: 1, nonce: 0, deadline: now + 86400, strictOrder: false, - scheduleId: '0x0303030303030303030303030303030303030303030303030303030303030303', - dueTimes: [now], + scheduleId: '0x0606060606060606060606060606060606060606060606060606060606060606', + dueTimes: [now - 1], initialLegs: [], - recurringLegs: [{ recipient: recipientAddress, amount: 10, paymentReference: ref(0x21) }], + recurringLegs: [{ recipient: recipientAddress, amount: 100, paymentReference: ref(0x41) }], + ...overrides, }; }; - it('blocks the single-fee entry point after the subscriber cancels', async () => { - await testERC20.transfer(subscriberAddress, 500); + it('reverts an under-funded pull, leaves the bitmap unset, and stays collectable after funding', async () => { + await testERC20.transfer(subscriberAddress, 50); await testERC20.connect(subscriber).approve(erc20RecurringPaymentProxy.address, 500); - const now = await latestTs(); - const permit = createSchedulePermit({ firstPayment: now, deadline: now + 86400 }); - const signature = await createSignature(permit, subscriber); + const permit = await pullPermit(testERC20.address); + const signature = await createBatchSignature(permit, subscriber); + const scheduleKey = await erc20RecurringPaymentProxy.scheduleKeyFromBatch(permit); - await erc20RecurringPaymentProxy.connect(subscriber).cancelSchedule(permit); + await expect( + erc20RecurringPaymentProxy + .connect(relayer) + .triggerRecurringPaymentBatch(permit, signature, 1), + ).to.be.reverted; + expect(await erc20RecurringPaymentProxy.triggeredPaymentsBitmap(scheduleKey)).to.equal(0); + + await testERC20.transfer(subscriberAddress, 500); + await erc20RecurringPaymentProxy + .connect(relayer) + .triggerRecurringPaymentBatch(permit, signature, 1); + expect(await erc20RecurringPaymentProxy.triggeredPaymentsBitmap(scheduleKey)).to.not.equal(0); + expect(await testERC20.balanceOf(erc20RecurringPaymentProxy.address)).to.equal(0); + }); + + it('cannot settle an unfunded subscriber from a residual proxy balance', async () => { + const SilentFailFactory = await ethers.getContractFactory('ERC20SilentFail'); + const silentFail = await SilentFailFactory.deploy(1000); + await silentFail.deployed(); + + await silentFail.transfer(erc20RecurringPaymentProxy.address, 500); + + const permit = await pullPermit(silentFail.address); + const signature = await createBatchSignature(permit, subscriber); + const scheduleKey = await erc20RecurringPaymentProxy.scheduleKeyFromBatch(permit); await expect( erc20RecurringPaymentProxy .connect(relayer) - .triggerRecurringPayment(permit, signature, 1, paymentReference), - ).to.be.revertedWith('ERC20RecurringPaymentProxy__Cancelled'); + .triggerRecurringPaymentBatch(permit, signature, 1), + ).to.be.revertedWith('ERC20RecurringPaymentProxy__TransferFailed'); + expect(await erc20RecurringPaymentProxy.triggeredPaymentsBitmap(scheduleKey)).to.equal(0); + expect(await silentFail.balanceOf(erc20RecurringPaymentProxy.address)).to.equal(500); + expect(await silentFail.balanceOf(recipientAddress)).to.equal(0); }); - it('blocks the batch entry point after the subscriber cancels', async () => { - await testERC20.transfer(subscriberAddress, 500); - await testERC20.connect(subscriber).approve(erc20RecurringPaymentProxy.address, 500); + it('reverts a fee-on-transfer token that under-delivers', async () => { + const FeeOnTransferFactory = await ethers.getContractFactory('ERC20FeeOnTransfer'); + const feeOnTransfer = await FeeOnTransferFactory.deploy(1000); + await feeOnTransfer.deployed(); - const permit = await simpleBatch(); + await feeOnTransfer.transfer(subscriberAddress, 500); + await feeOnTransfer.connect(subscriber).approve(erc20RecurringPaymentProxy.address, 500); + + const permit = await pullPermit(feeOnTransfer.address); const signature = await createBatchSignature(permit, subscriber); - await erc20RecurringPaymentProxy.connect(subscriber).cancelScheduleBatch(permit); + const scheduleKey = await erc20RecurringPaymentProxy.scheduleKeyFromBatch(permit); await expect( erc20RecurringPaymentProxy .connect(relayer) .triggerRecurringPaymentBatch(permit, signature, 1), - ).to.be.revertedWith('ERC20RecurringPaymentProxy__Cancelled'); + ).to.be.revertedWith('ERC20RecurringPaymentProxy__ShortPull'); + expect(await erc20RecurringPaymentProxy.triggeredPaymentsBitmap(scheduleKey)).to.equal(0); + }); + + it('reverts when the token returns false without reverting', async () => { + const SilentFailFactory = await ethers.getContractFactory('ERC20SilentFail'); + const silentFail = await SilentFailFactory.deploy(1000); + await silentFail.deployed(); + + await silentFail.transfer(subscriberAddress, 500); + // No approve: transferFrom returns false instead of reverting. + + const permit = await pullPermit(silentFail.address); + const signature = await createBatchSignature(permit, subscriber); + const scheduleKey = await erc20RecurringPaymentProxy.scheduleKeyFromBatch(permit); + + await expect( + erc20RecurringPaymentProxy + .connect(relayer) + .triggerRecurringPaymentBatch(permit, signature, 1), + ).to.be.revertedWith('ERC20RecurringPaymentProxy__TransferFailed'); + expect(await erc20RecurringPaymentProxy.triggeredPaymentsBitmap(scheduleKey)).to.equal(0); }); - it('keeps a cancelled single-fee schedule cancelled after a deadline re-sign', async () => { + it('does not mark the cycle paid when the relayer-fee transfer fails', async () => { + const FailTransferFactory = await ethers.getContractFactory('ERC20FailTransfer'); + const failTransfer = await FailTransferFactory.deploy(1000); + await failTransfer.deployed(); + + await failTransfer.transfer(subscriberAddress, 500); + await failTransfer.connect(subscriber).approve(erc20RecurringPaymentProxy.address, 500); + + const permit = await pullPermit(failTransfer.address); + const signature = await createBatchSignature(permit, subscriber); + const scheduleKey = await erc20RecurringPaymentProxy.scheduleKeyFromBatch(permit); + + await expect( + erc20RecurringPaymentProxy + .connect(relayer) + .triggerRecurringPaymentBatch(permit, signature, 1), + ).to.be.revertedWith('ERC20RecurringPaymentProxy__TransferFailed'); + expect(await erc20RecurringPaymentProxy.triggeredPaymentsBitmap(scheduleKey)).to.equal(0); + expect(await failTransfer.balanceOf(recipientAddress)).to.equal(0); + }); + }); + + describe('EIP-1271 signatures', () => { + const ref = (n: number) => ethers.utils.hexZeroPad(ethers.utils.hexlify(n), 8); + + const walletPermit = async (wallet: string) => { + const now = (await ethers.provider.getBlock('latest')).timestamp; + return { + subscriber: wallet, + token: testERC20.address, + relayerFee: 5, + totalPayments: 1, + nonce: 0, + deadline: now + 86400, + strictOrder: false, + scheduleId: '0x0707070707070707070707070707070707070707070707070707070707070707', + dueTimes: [now - 1], + initialLegs: [], + recurringLegs: [{ recipient: recipientAddress, amount: 100, paymentReference: ref(0x51) }], + }; + }; + + it('accepts a valid smart-account signature', async () => { + const MockERC1271Factory = await ethers.getContractFactory('MockERC1271'); + const mockWallet = await MockERC1271Factory.deploy(subscriberAddress); + await mockWallet.deployed(); + + await testERC20.transfer(mockWallet.address, 500); + await mockWallet + .connect(subscriber) + .approveToken(testERC20.address, erc20RecurringPaymentProxy.address, 500); + + const permit = await walletPermit(mockWallet.address); + const signature = await createBatchSignature(permit, subscriber); + + await expect( + erc20RecurringPaymentProxy + .connect(relayer) + .triggerRecurringPaymentBatch(permit, signature, 1), + ) + .to.emit(erc20FeeProxy, 'TransferWithReferenceAndFee') + .withArgs( + testERC20.address, + recipientAddress, + 100, + ethers.utils.keccak256(ref(0x51)), + 0, + ethers.constants.AddressZero, + ); + }); + + it('rejects a malformed smart-account signature', async () => { + const MockERC1271Factory = await ethers.getContractFactory('MockERC1271'); + const mockWallet = await MockERC1271Factory.deploy(subscriberAddress); + await mockWallet.deployed(); + + await testERC20.transfer(mockWallet.address, 500); + await mockWallet + .connect(subscriber) + .approveToken(testERC20.address, erc20RecurringPaymentProxy.address, 500); + + const permit = await walletPermit(mockWallet.address); + const signature = '0x' + '11'.repeat(65); + + await expect( + erc20RecurringPaymentProxy + .connect(relayer) + .triggerRecurringPaymentBatch(permit, signature, 1), + ).to.be.revertedWith('ERC20RecurringPaymentProxy__BadSignature'); + }); + }); + + describe('cancelSchedule', () => { + const ref = (n: number) => ethers.utils.hexZeroPad(ethers.utils.hexlify(n), 8); + + const latestTs = async () => (await ethers.provider.getBlock('latest')).timestamp; + + const simpleBatch = async () => { const now = await latestTs(); - const permit = createSchedulePermit({ firstPayment: now, deadline: now + 86400 }); - await erc20RecurringPaymentProxy.connect(subscriber).cancelSchedule(permit); + return { + subscriber: subscriberAddress, + token: testERC20.address, + relayerFee: 0, + totalPayments: 1, + nonce: 0, + deadline: now + 86400, + strictOrder: false, + scheduleId: '0x0303030303030303030303030303030303030303030303030303030303030303', + dueTimes: [now], + initialLegs: [], + recurringLegs: [{ recipient: recipientAddress, amount: 10, paymentReference: ref(0x21) }], + }; + }; - const resigned = { ...permit, deadline: now + 86400 * 30 }; - const signature = await createSignature(resigned, subscriber); + it('blocks the batch entry point after the subscriber cancels', async () => { await testERC20.transfer(subscriberAddress, 500); await testERC20.connect(subscriber).approve(erc20RecurringPaymentProxy.address, 500); + const permit = await simpleBatch(); + const signature = await createBatchSignature(permit, subscriber); + await erc20RecurringPaymentProxy.connect(subscriber).cancelScheduleBatch(permit); + await expect( erc20RecurringPaymentProxy .connect(relayer) - .triggerRecurringPayment(resigned, signature, 1, paymentReference), + .triggerRecurringPaymentBatch(permit, signature, 1), ).to.be.revertedWith('ERC20RecurringPaymentProxy__Cancelled'); }); it('reverts when a non-subscriber tries to cancel', async () => { - const permit = createSchedulePermit(); - await expect( - erc20RecurringPaymentProxy.connect(relayer).cancelSchedule(permit), - ).to.be.revertedWith('ERC20RecurringPaymentProxy__NotSubscriber'); await expect( erc20RecurringPaymentProxy.connect(user).cancelScheduleBatch(await simpleBatch()), ).to.be.revertedWith('ERC20RecurringPaymentProxy__NotSubscriber'); }); - - it("reverts when another subscriber tries to cancel someone else's schedule", async () => { - const permit = createSchedulePermit(); - const hijack = { ...permit, subscriber: userAddress }; - await expect(erc20RecurringPaymentProxy.connect(user).cancelSchedule(hijack)).to.not.be - .reverted; - expect( - await erc20RecurringPaymentProxy.cancelledSchedules( - await erc20RecurringPaymentProxy.scheduleKeyFromPermit(permit), - ), - ).to.equal(false); - }); }); describe('admitCycles', () => { - const ref = (n: number) => ethers.utils.hexZeroPad(ethers.utils.hexlify(n), 32); + const ref = (n: number) => ethers.utils.hexZeroPad(ethers.utils.hexlify(n), 8); const latestTs = async () => (await ethers.provider.getBlock('latest')).timestamp; const bit = (index: number) => ethers.BigNumber.from(1).shl(index); @@ -1572,20 +1069,6 @@ describe('ERC20RecurringPaymentProxy', () => { ).to.be.revertedWith('Pausable: paused'); }); - it('keeps the single-fee entry point relayer-only', async () => { - const now = await latestTs(); - const permit = createSchedulePermit({ firstPayment: now, deadline: now + 86400 }); - const signature = await createSignature(permit, subscriber); - const scheduleKey = await erc20RecurringPaymentProxy.scheduleKeyFromPermit(permit); - await erc20RecurringPaymentProxy.connect(relayer).admitCycles(scheduleKey, bit(1)); - - await expect( - erc20RecurringPaymentProxy - .connect(subscriber) - .triggerRecurringPayment(permit, signature, 1, '0x1234567890abcdef'), - ).to.be.revertedWith('AccessControl: account'); - }); - it('reverts when a non-relayer tries to admit cycles', async () => { const permit = await batchPermit(); const scheduleKey = await erc20RecurringPaymentProxy.scheduleKeyFromBatch(permit); @@ -1596,7 +1079,7 @@ describe('ERC20RecurringPaymentProxy', () => { }); describe('revokeCycles', () => { - const ref = (n: number) => ethers.utils.hexZeroPad(ethers.utils.hexlify(n), 32); + const ref = (n: number) => ethers.utils.hexZeroPad(ethers.utils.hexlify(n), 8); const latestTs = async () => (await ethers.provider.getBlock('latest')).timestamp; const bit = (index: number) => ethers.BigNumber.from(1).shl(index); @@ -1696,13 +1179,6 @@ describe('ERC20RecurringPaymentProxy', () => { }; }; - it('matches ethers _TypedDataEncoder for SchedulePermit', async () => { - const permit = createSchedulePermit(); - expect(await erc20RecurringPaymentProxy.hashSchedule(permit)).to.equal( - await hashPermitOffchain(permit), - ); - }); - it('matches ethers _TypedDataEncoder for the worked-example SchedulePermitBatch', async () => { const permit = workedExample(); expect(await erc20RecurringPaymentProxy.hashScheduleBatch(permit)).to.equal(