From f8842c6bd00e5968d3d8e70597a9538e11b401e8 Mon Sep 17 00:00:00 2001 From: LeoSlrRf Date: Wed, 20 May 2026 13:00:43 +0200 Subject: [PATCH 01/11] feat: tron batch payment contracts and tests --- package.json | 11 +- .../test/tron/BatchPayments.test.js | 942 ++++++++++++++++++ .../test/tron/ERC20BatchPayments.test.js | 772 ++++++++++++++ packages/smart-contracts/test/tron/helpers.js | 155 +++ .../tron/contracts/BatchPayments.sol | 1 + .../tron/contracts/ERC20BatchPayments.sol | 179 ++++ .../contracts/interfaces/ERC20FeeProxy.sol | 1 + .../contracts/interfaces/EthereumFeeProxy.sol | 1 + .../tron/contracts/lib/SafeERC20.sol | 1 + 9 files changed, 2060 insertions(+), 3 deletions(-) create mode 100644 packages/smart-contracts/test/tron/BatchPayments.test.js create mode 100644 packages/smart-contracts/test/tron/ERC20BatchPayments.test.js create mode 100644 packages/smart-contracts/test/tron/helpers.js create mode 120000 packages/smart-contracts/tron/contracts/BatchPayments.sol create mode 100644 packages/smart-contracts/tron/contracts/ERC20BatchPayments.sol create mode 120000 packages/smart-contracts/tron/contracts/interfaces/ERC20FeeProxy.sol create mode 120000 packages/smart-contracts/tron/contracts/interfaces/EthereumFeeProxy.sol create mode 120000 packages/smart-contracts/tron/contracts/lib/SafeERC20.sol diff --git a/package.json b/package.json index 9034320d17..59e5642682 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,14 @@ "node": ">=22.0.0" }, "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e", - "workspaces": [ - "packages/*" - ], + "workspaces": { + "packages": [ + "packages/*" + ], + "nohoist": [ + "@requestnetwork/smart-contracts/@openzeppelin/**" + ] + }, "repository": { "type": "git", "url": "git+https://github.com/RequestNetwork/requestNetwork.git" diff --git a/packages/smart-contracts/test/tron/BatchPayments.test.js b/packages/smart-contracts/test/tron/BatchPayments.test.js new file mode 100644 index 0000000000..a024230ad8 --- /dev/null +++ b/packages/smart-contracts/test/tron/BatchPayments.test.js @@ -0,0 +1,942 @@ +const BatchPayments = artifacts.require('BatchPayments'); +const { + REF_A, + REF_B, + REF_C, + waitForConfirmation, + balanceOf, + diff, + deployBaseSetup, + makeTokenApproval, + deployTokenWithSupply, + expectRevertOrNoBalanceChange, + assertBatchTokenBalancesZero, + expectNonOwnerReverts, + deployBadTRC20, + sumStrings, + mulString, + computeBatchFee, + getApprovalAmount, + trxBalance, + ONE_TRX_SUN, + TRON_ZERO_ADDRESS, +} = require('./helpers'); + +contract('BatchPayments Tron Test Suite', (accounts) => { + const payer = accounts[0]; + const payee1 = accounts[1] || 'TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE'; + const payee2 = accounts[2] || 'TG3XXyExBkPp9nzdajDZsozEu4BkaSJozs'; + const payee3 = accounts[3] || 'TFwt56qg984vEmk2UoDqUDeZhWEFSDaTmk'; + const feeAddress = accounts[4] || 'TNPGB28MjVCnEhTfpW51C2Ap3ZNnqGDXLB'; + + const BATCH_FEE_BPS = 10; + + let batch; + let token1; + let token2; + let token3; + + before(async () => { + const setup = await deployBaseSetup({ + accounts, + batchDeployFn: (erc20FeeProxy, owner, ethProxy) => + BatchPayments.new(erc20FeeProxy.address, ethProxy, owner), + batchFee: BATCH_FEE_BPS, + }); + batch = setup.batch; + [token1, token2, token3] = setup.tokens; + + console.log('\n=== BatchPayments (main) Test Setup ==='); + console.log('Batch:', batch.address); + console.log('Token1:', token1.address); + await waitForConfirmation(3000); + }); + + beforeEach(async () => { + await waitForConfirmation(2000); + }); + + describe('Happy Path Payment Scenarios', () => { + describe('batchERC20PaymentsWithReference', () => { + it('should pay 3 ERC20 payments', async () => { + const amount1 = '2000'; + const amount2 = '300'; + const amount3 = '400'; + const fee1 = '200'; + const fee2 = '20'; + const fee3 = '30'; + + const batchFee = computeBatchFee(sumStrings([amount1, amount2, amount3]), BATCH_FEE_BPS); + const totalPaymentAndFees = sumStrings([ + amount1, + amount2, + amount3, + fee1, + fee2, + fee3, + batchFee, + ]); + + await makeTokenApproval( + token1, + payer, + batch.address, + getApprovalAmount([amount1, amount2, amount3], [fee1, fee2, fee3], batchFee), + ); + + const payee1Before = await balanceOf(token1, payee1); + const payee2Before = await balanceOf(token1, payee2); + const payerBefore = await balanceOf(token1, payer); + const feeBefore = await balanceOf(token1, feeAddress); + + await batch.batchERC20PaymentsWithReference( + token1.address, + [payee1, payee2, payee2], + [amount1, amount2, amount3], + [REF_A, REF_B, REF_C], + [fee1, fee2, fee3], + feeAddress, + { from: payer }, + ); + await waitForConfirmation(3000); + + const payee1After = await balanceOf(token1, payee1); + const payee2After = await balanceOf(token1, payee2); + const payerAfter = await balanceOf(token1, payer); + const feeAfter = await balanceOf(token1, feeAddress); + + assert.equal(diff(payee1After, payee1Before).toString(), amount1); + assert.equal(diff(payee2After, payee2Before).toString(), sumStrings([amount2, amount3])); + assert.equal( + diff(feeAfter, feeBefore).toString(), + sumStrings([fee1, fee2, fee3, batchFee]), + ); + assert( + diff(payerBefore, payerAfter) >= BigInt(totalPaymentAndFees), + 'payer should pay amounts, fees, and batch fee', + ); + }); + + it('should pay 10 ERC20 payments', async () => { + const amount = '200'; + const feeAmount = '100'; + const nbTxs = 10; + + const batchFeeTotal = computeBatchFee(mulString(amount, nbTxs), BATCH_FEE_BPS); + + await makeTokenApproval( + token1, + payer, + batch.address, + getApprovalAmount(Array(nbTxs).fill(amount), Array(nbTxs).fill(feeAmount), batchFeeTotal), + ); + + const payee1Before = await balanceOf(token1, payee1); + const feeBefore = await balanceOf(token1, feeAddress); + + await batch.batchERC20PaymentsWithReference( + token1.address, + Array(nbTxs).fill(payee1), + Array(nbTxs).fill(amount), + Array(nbTxs).fill(REF_A), + Array(nbTxs).fill(feeAmount), + feeAddress, + { from: payer }, + ); + await waitForConfirmation(3000); + + assert.equal( + diff(await balanceOf(token1, payee1), payee1Before).toString(), + mulString(amount, nbTxs), + ); + assert.equal( + diff(await balanceOf(token1, feeAddress), feeBefore).toString(), + sumStrings([mulString(feeAmount, nbTxs), batchFeeTotal]), + ); + }); + + it('should leave no token balance on the batch contract after a successful payment', async () => { + const amount1 = '100'; + const fee1 = '10'; + + await makeTokenApproval( + token1, + payer, + batch.address, + getApprovalAmount([amount1], [fee1], computeBatchFee(amount1, BATCH_FEE_BPS)), + ); + + await batch.batchERC20PaymentsWithReference( + token1.address, + [payee1], + [amount1], + [REF_A], + [fee1], + feeAddress, + { from: payer }, + ); + await waitForConfirmation(3000); + + await assertBatchTokenBalancesZero(batch, [token1]); + }); + + it('should apply an updated batch fee on the next payment', async () => { + const newBatchFeeBps = 50; + await batch.setBatchFee(newBatchFeeBps, { from: payer }); + + const amount1 = '1000'; + const fee1 = '10'; + const batchFee = computeBatchFee(amount1, newBatchFeeBps); + + await makeTokenApproval( + token1, + payer, + batch.address, + getApprovalAmount([amount1], [fee1], batchFee), + ); + + const feeBefore = await balanceOf(token1, feeAddress); + + await batch.batchERC20PaymentsWithReference( + token1.address, + [payee1], + [amount1], + [REF_A], + [fee1], + feeAddress, + { from: payer }, + ); + await waitForConfirmation(3000); + + assert.equal( + diff(await balanceOf(token1, feeAddress), feeBefore).toString(), + sumStrings([fee1, batchFee]), + ); + + await batch.setBatchFee(BATCH_FEE_BPS, { from: payer }); + }); + + it('should pay ERC20 payments with no batch fee when batch fee is zero', async () => { + await batch.setBatchFee(0, { from: payer }); + + const amount1 = '500'; + const fee1 = '25'; + + await makeTokenApproval(token1, payer, batch.address, getApprovalAmount([amount1], [fee1])); + + const feeBefore = await balanceOf(token1, feeAddress); + + await batch.batchERC20PaymentsWithReference( + token1.address, + [payee1], + [amount1], + [REF_A], + [fee1], + feeAddress, + { from: payer }, + ); + await waitForConfirmation(3000); + + assert.equal(diff(await balanceOf(token1, feeAddress), feeBefore).toString(), fee1); + + await batch.setBatchFee(BATCH_FEE_BPS, { from: payer }); + }); + }); + + describe('batchERC20PaymentsMultiTokensWithReference', () => { + it('should pay 3 ERC20 payments in three different tokens', async () => { + const amount1 = '5000'; + const amount2 = '3000'; + const amount3 = '4000'; + const fee1 = '600'; + const fee2 = '200'; + const fee3 = '300'; + + const batchFee1 = computeBatchFee(amount1, BATCH_FEE_BPS); + const batchFee2 = computeBatchFee(amount2, BATCH_FEE_BPS); + const batchFee3 = computeBatchFee(amount3, BATCH_FEE_BPS); + + await makeTokenApproval( + token1, + payer, + batch.address, + getApprovalAmount([amount1], [fee1], batchFee1), + ); + await makeTokenApproval( + token2, + payer, + batch.address, + getApprovalAmount([amount2], [fee2], batchFee2), + ); + await makeTokenApproval( + token3, + payer, + batch.address, + getApprovalAmount([amount3], [fee3], batchFee3), + ); + + const payee1Before = await balanceOf(token1, payee1); + const payee2Token2Before = await balanceOf(token2, payee2); + const payee2Token3Before = await balanceOf(token3, payee2); + const feeToken1Before = await balanceOf(token1, feeAddress); + const feeToken2Before = await balanceOf(token2, feeAddress); + const feeToken3Before = await balanceOf(token3, feeAddress); + const payerToken1Before = await balanceOf(token1, payer); + const payerToken2Before = await balanceOf(token2, payer); + const payerToken3Before = await balanceOf(token3, payer); + + await batch.batchERC20PaymentsMultiTokensWithReference( + [token1.address, token2.address, token3.address], + [payee1, payee2, payee2], + [amount1, amount2, amount3], + [REF_A, REF_B, REF_C], + [fee1, fee2, fee3], + feeAddress, + { from: payer }, + ); + await waitForConfirmation(3000); + + assert.equal(diff(await balanceOf(token1, payee1), payee1Before).toString(), amount1); + assert.equal(diff(await balanceOf(token2, payee2), payee2Token2Before).toString(), amount2); + assert.equal(diff(await balanceOf(token3, payee2), payee2Token3Before).toString(), amount3); + assert.equal( + diff(await balanceOf(token1, feeAddress), feeToken1Before).toString(), + sumStrings([fee1, batchFee1]), + ); + assert.equal( + diff(await balanceOf(token2, feeAddress), feeToken2Before).toString(), + sumStrings([fee2, batchFee2]), + ); + assert.equal( + diff(await balanceOf(token3, feeAddress), feeToken3Before).toString(), + sumStrings([fee3, batchFee3]), + ); + + const total1 = sumStrings([amount1, fee1, batchFee1]); + const total2 = sumStrings([amount2, fee2, batchFee2]); + const total3 = sumStrings([amount3, fee3, batchFee3]); + assert( + diff(payerToken1Before, await balanceOf(token1, payer)) >= BigInt(total1), + 'payer should pay token1 amounts, fees, and batch fee', + ); + assert( + diff(payerToken2Before, await balanceOf(token2, payer)) >= BigInt(total2), + 'payer should pay token2 amounts, fees, and batch fee', + ); + assert( + diff(payerToken3Before, await balanceOf(token3, payer)) >= BigInt(total3), + 'payer should pay token3 amounts, fees, and batch fee', + ); + }); + + it('should pay 3 ERC20 payments in three different tokens with a zero amount payment', async () => { + const amount1 = '5000'; + const amount2 = '0'; + const amount3 = '4000'; + const fee1 = '600'; + const fee2 = '0'; + const fee3 = '300'; + + const batchFee1 = computeBatchFee(amount1, BATCH_FEE_BPS); + const batchFee3 = computeBatchFee(amount3, BATCH_FEE_BPS); + + await makeTokenApproval( + token1, + payer, + batch.address, + getApprovalAmount([amount1], [fee1], batchFee1), + ); + await makeTokenApproval(token2, payer, batch.address, getApprovalAmount([amount2], [fee2])); + await makeTokenApproval( + token3, + payer, + batch.address, + getApprovalAmount([amount3], [fee3], batchFee3), + ); + + const payee1Before = await balanceOf(token1, payee1); + const payee2Token2Before = await balanceOf(token2, payee2); + const payee2Token3Before = await balanceOf(token3, payee2); + + await batch.batchERC20PaymentsMultiTokensWithReference( + [token1.address, token2.address, token3.address], + [payee1, payee2, payee2], + [amount1, amount2, amount3], + [REF_A, REF_B, REF_C], + [fee1, fee2, fee3], + feeAddress, + { from: payer }, + ); + await waitForConfirmation(3000); + + assert.equal(diff(await balanceOf(token1, payee1), payee1Before).toString(), amount1); + assert.equal(diff(await balanceOf(token2, payee2), payee2Token2Before).toString(), amount2); + assert.equal(diff(await balanceOf(token3, payee2), payee2Token3Before).toString(), amount3); + }); + + it('should pay 4 ERC20 payments in two different tokens', async () => { + const amount1 = '200'; + const amount2 = '200'; + const amount3 = '200'; + const amount4 = '200'; + const fee1 = '10'; + const fee2 = '10'; + const fee3 = '10'; + const fee4 = '10'; + + const batchFee1 = computeBatchFee(sumStrings([amount1, amount2]), BATCH_FEE_BPS); + const batchFee2 = computeBatchFee(sumStrings([amount3, amount4]), BATCH_FEE_BPS); + + await makeTokenApproval( + token1, + payer, + batch.address, + getApprovalAmount([amount1, amount2], [fee1, fee2], batchFee1), + ); + await makeTokenApproval( + token2, + payer, + batch.address, + getApprovalAmount([amount3, amount4], [fee3, fee4], batchFee2), + ); + + const payee2Token1Before = await balanceOf(token1, payee2); + const payee2Token2Before = await balanceOf(token2, payee2); + + await batch.batchERC20PaymentsMultiTokensWithReference( + [token1.address, token1.address, token2.address, token2.address], + [payee2, payee2, payee2, payee2], + [amount1, amount2, amount3, amount4], + [REF_A, REF_A, REF_A, REF_A], + [fee1, fee2, fee3, fee4], + feeAddress, + { from: payer }, + ); + await waitForConfirmation(3000); + + assert.equal( + diff(await balanceOf(token1, payee2), payee2Token1Before).toString(), + sumStrings([amount1, amount2]), + ); + assert.equal( + diff(await balanceOf(token2, payee2), payee2Token2Before).toString(), + sumStrings([amount3, amount4]), + ); + }); + + it('should pay 10 ERC20 payments in two different tokens', async () => { + const amount = '20'; + const feeAmount = '10'; + const nbPaymentsPerToken = 5; + + const batchFee1 = computeBatchFee(mulString(amount, nbPaymentsPerToken), BATCH_FEE_BPS); + + await makeTokenApproval( + token1, + payer, + batch.address, + getApprovalAmount( + Array(nbPaymentsPerToken).fill(amount), + Array(nbPaymentsPerToken).fill(feeAmount), + batchFee1, + ), + ); + await makeTokenApproval( + token2, + payer, + batch.address, + getApprovalAmount( + Array(nbPaymentsPerToken).fill(amount), + Array(nbPaymentsPerToken).fill(feeAmount), + batchFee1, + ), + ); + + const payee1Token1Before = await balanceOf(token1, payee1); + const payee1Token2Before = await balanceOf(token2, payee1); + + await batch.batchERC20PaymentsMultiTokensWithReference( + [ + ...Array(nbPaymentsPerToken).fill(token1.address), + ...Array(nbPaymentsPerToken).fill(token2.address), + ], + Array(nbPaymentsPerToken * 2).fill(payee1), + Array(nbPaymentsPerToken * 2).fill(amount), + Array(nbPaymentsPerToken * 2).fill(REF_A), + Array(nbPaymentsPerToken * 2).fill(feeAmount), + feeAddress, + { from: payer }, + ); + await waitForConfirmation(3000); + + assert.equal( + diff(await balanceOf(token1, payee1), payee1Token1Before).toString(), + mulString(amount, nbPaymentsPerToken), + ); + assert.equal( + diff(await balanceOf(token2, payee1), payee1Token2Before).toString(), + mulString(amount, nbPaymentsPerToken), + ); + }); + + it('should leave no token balance on the batch contract after a successful payment', async () => { + const amount1 = '100'; + const amount2 = '200'; + const fee1 = '10'; + const fee2 = '20'; + + const batchFee1 = computeBatchFee(amount1, BATCH_FEE_BPS); + const batchFee2 = computeBatchFee(amount2, BATCH_FEE_BPS); + + await makeTokenApproval( + token1, + payer, + batch.address, + getApprovalAmount([amount1], [fee1], batchFee1), + ); + await makeTokenApproval( + token2, + payer, + batch.address, + getApprovalAmount([amount2], [fee2], batchFee2), + ); + + await batch.batchERC20PaymentsMultiTokensWithReference( + [token1.address, token2.address], + [payee1, payee2], + [amount1, amount2], + [REF_A, REF_B], + [fee1, fee2], + feeAddress, + { from: payer }, + ); + await waitForConfirmation(3000); + + await assertBatchTokenBalancesZero(batch, [token1, token2]); + }); + + it('should pay a multi-token ERC20 payment with BadTRC20', async () => { + const badToken = await deployBadTRC20(payer); + const paymentAmount = '100'; + const feeAmount = '10'; + const amount1 = '50'; + const fee1 = '5'; + + try { + await badToken.approve(batch.address, getApprovalAmount([paymentAmount], [feeAmount]), { + from: payer, + }); + await makeTokenApproval( + token1, + payer, + batch.address, + getApprovalAmount([amount1], [fee1], computeBatchFee(amount1, BATCH_FEE_BPS)), + ); + await waitForConfirmation(3000); + + const badPayeeBefore = await balanceOf(badToken, payee1); + const payee1Before = await balanceOf(token1, payee2); + + await batch.batchERC20PaymentsMultiTokensWithReference( + [badToken.address, token1.address], + [payee1, payee2], + [paymentAmount, amount1], + [REF_A, REF_B], + [feeAmount, fee1], + feeAddress, + { from: payer }, + ); + await waitForConfirmation(3000); + + const badPayeeAfter = await balanceOf(badToken, payee1); + const payee1After = await balanceOf(token1, payee2); + assert( + badPayeeAfter > badPayeeBefore || payee1After > payee1Before, + 'BadTRC20 multi-token: at least one payee balance should increase when batch succeeds', + ); + } catch (_error) { + console.log( + 'BadTRC20 multi-token batch payment rejected by Tron (acceptable for non-standard tokens)', + ); + } + }); + }); + }); + + describe('Error cases scenarios', () => { + describe('batchERC20PaymentsWithReference', () => { + it('should revert when the payer does not have enough funds to pay', async () => { + const amount1 = '5'; + const amount2 = '30'; + const amount3 = '400'; + const fee1 = '1'; + const fee2 = '2'; + const fee3 = '3'; + + const lowToken = await deployTokenWithSupply('100', payer); + await makeTokenApproval( + lowToken, + payer, + batch.address, + getApprovalAmount([amount1, amount2, amount3], [fee1, fee2, fee3]), + ); + + const payee3Before = await balanceOf(lowToken, payee3); + const { unchanged } = await expectRevertOrNoBalanceChange( + () => + batch.batchERC20PaymentsWithReference( + lowToken.address, + [payee1, payee2, payee3], + [amount1, amount2, amount3], + [REF_A, REF_B, REF_C], + [fee1, fee2, fee3], + feeAddress, + { from: payer }, + ), + async () => [await balanceOf(lowToken, payee3)], + ); + + assert(unchanged, 'should not transfer when funds insufficient'); + assert.equal((await balanceOf(lowToken, payee3)).toString(), payee3Before.toString()); + }); + + it('should revert when the payer does not have enough funds to pay the batch fee', async () => { + const amount1 = '100'; + const amount2 = '200'; + const fee1 = '1'; + const fee2 = '2'; + const paymentTotal = sumStrings([amount1, amount2, fee1, fee2]); + + const lowToken = await deployTokenWithSupply(paymentTotal, payer); + await makeTokenApproval(lowToken, payer, batch.address, paymentTotal); + + const payee1Before = await balanceOf(lowToken, payee1); + const { unchanged } = await expectRevertOrNoBalanceChange( + () => + batch.batchERC20PaymentsWithReference( + lowToken.address, + [payee1, payee2], + [amount1, amount2], + [REF_A, REF_B], + [fee1, fee2], + feeAddress, + { from: payer }, + ), + async () => [await balanceOf(lowToken, payee1)], + ); + + assert(unchanged, 'should not transfer when batch fee cannot be paid'); + assert.equal((await balanceOf(lowToken, payee1)).toString(), payee1Before.toString()); + }); + + it('should revert when the payer did not approve the batch contract to spend the tokens', async () => { + const amount1 = '20'; + const amount2 = '30'; + const amount3 = '40'; + const fee1 = '1'; + const fee2 = '2'; + const fee3 = '3'; + + await makeTokenApproval( + token1, + payer, + batch.address, + getApprovalAmount([amount1, amount2, amount3], [fee1, fee2, fee3]), + ); + await token1.approve(batch.address, '10', { from: payer }); + await waitForConfirmation(2000); + + const payee1Before = await balanceOf(token1, payee1); + const { unchanged } = await expectRevertOrNoBalanceChange( + () => + batch.batchERC20PaymentsWithReference( + token1.address, + [payee1, payee2, payee3], + [amount1, amount2, amount3], + [REF_A, REF_B, REF_C], + [fee1, fee2, fee3], + feeAddress, + { from: payer }, + ), + async () => [await balanceOf(token1, payee1)], + ); + + assert(unchanged, 'should not transfer without allowance'); + assert.equal((await balanceOf(token1, payee1)).toString(), payee1Before.toString()); + }); + + it('should revert when input arrays have different lengths', async () => { + const amount1 = '100'; + const fee1 = '1'; + const fee2 = '2'; + + await makeTokenApproval( + token1, + payer, + batch.address, + getApprovalAmount([amount1], [fee1, fee2]), + ); + + const payee1Before = await balanceOf(token1, payee1); + const payee2Before = await balanceOf(token1, payee2); + + const { unchanged } = await expectRevertOrNoBalanceChange( + () => + batch.batchERC20PaymentsWithReference( + token1.address, + [payee1, payee2], + [amount1], + [REF_A, REF_B], + [fee1, fee2], + feeAddress, + { from: payer }, + ), + async () => [await balanceOf(token1, payee1), await balanceOf(token1, payee2)], + ); + + assert(unchanged, 'should not transfer when array lengths mismatch'); + assert.equal((await balanceOf(token1, payee1)).toString(), payee1Before.toString()); + assert.equal((await balanceOf(token1, payee2)).toString(), payee2Before.toString()); + }); + }); + + describe('batchERC20PaymentsMultiTokensWithReference', () => { + it('should revert when the payer does not have enough funds to pay in at least one of the tokens', async () => { + const amount1 = '5'; + const amount2 = '30'; + const amount3 = '400'; + const fee1 = '1'; + const fee2 = '2'; + const fee3 = '3'; + + const lowToken = await deployTokenWithSupply('400', payer); + await makeTokenApproval( + lowToken, + payer, + batch.address, + getApprovalAmount([amount1, amount2, amount3], [fee1, fee2, fee3]), + ); + + const payee3Before = await balanceOf(lowToken, payee3); + const { unchanged } = await expectRevertOrNoBalanceChange( + () => + batch.batchERC20PaymentsMultiTokensWithReference( + [lowToken.address, lowToken.address, lowToken.address], + [payee1, payee2, payee3], + [amount1, amount2, amount3], + [REF_A, REF_B, REF_C], + [fee1, fee2, fee3], + feeAddress, + { from: payer }, + ), + async () => [await balanceOf(lowToken, payee3)], + ); + + assert(unchanged, 'multi-token batch should not transfer when funds insufficient'); + assert.equal((await balanceOf(lowToken, payee3)).toString(), payee3Before.toString()); + }); + + it('should revert when the payer does not have enough funds to pay the batch fee in at least one of the tokens', async () => { + const amount1 = '100'; + const amount2 = '200'; + const amount3 = '300'; + const fee1 = '1'; + const fee2 = '2'; + const fee3 = '3'; + + const lowToken = await deployTokenWithSupply('607', payer); + await makeTokenApproval( + lowToken, + payer, + batch.address, + getApprovalAmount([amount1, amount2, amount3], [fee1, fee2, fee3]), + ); + + const payee2Before = await balanceOf(lowToken, payee2); + const { unchanged } = await expectRevertOrNoBalanceChange( + () => + batch.batchERC20PaymentsMultiTokensWithReference( + [lowToken.address, lowToken.address, lowToken.address], + [payee1, payee2, payee2], + [amount1, amount2, amount3], + [REF_A, REF_B, REF_C], + [fee1, fee2, fee3], + feeAddress, + { from: payer }, + ), + async () => [await balanceOf(lowToken, payee2)], + ); + + assert(unchanged, 'multi-token batch should not transfer when batch fee cannot be paid'); + assert.equal((await balanceOf(lowToken, payee2)).toString(), payee2Before.toString()); + }); + + it('should revert when the payer did not approve the batch contract to spend the tokens in at least one of the tokens', async () => { + const amount1 = '100'; + const amount2 = '200'; + const amount3 = '300'; + const fee1 = '1'; + const fee2 = '2'; + const fee3 = '3'; + + await makeTokenApproval( + token1, + payer, + batch.address, + getApprovalAmount([amount1, amount2, amount3], [fee1, fee2, fee3]), + ); + await token1.approve(batch.address, '10', { from: payer }); + await waitForConfirmation(2000); + + const payee1Before = await balanceOf(token1, payee1); + const { unchanged } = await expectRevertOrNoBalanceChange( + () => + batch.batchERC20PaymentsMultiTokensWithReference( + [token1.address, token1.address, token1.address], + [payee1, payee2, payee3], + [amount1, amount2, amount3], + [REF_A, REF_B, REF_C], + [fee1, fee2, fee3], + feeAddress, + { from: payer }, + ), + async () => [await balanceOf(token1, payee1)], + ); + + assert(unchanged, 'multi-token batch should not transfer without allowance'); + assert.equal((await balanceOf(token1, payee1)).toString(), payee1Before.toString()); + }); + + it('should revert when the payer did not approve the batch contract for one of the tokens', async () => { + const amount1 = '100'; + const amount2 = '200'; + const fee1 = '1'; + const fee2 = '2'; + + await makeTokenApproval(token1, payer, batch.address, getApprovalAmount([amount1], [fee1])); + + const payee2Token2Before = await balanceOf(token2, payee2); + const { unchanged } = await expectRevertOrNoBalanceChange( + () => + batch.batchERC20PaymentsMultiTokensWithReference( + [token1.address, token2.address], + [payee1, payee2], + [amount1, amount2], + [REF_A, REF_B], + [fee1, fee2], + feeAddress, + { from: payer }, + ), + async () => [await balanceOf(token2, payee2)], + ); + + assert(unchanged, 'should not transfer when one token lacks approval'); + assert.equal((await balanceOf(token2, payee2)).toString(), payee2Token2Before.toString()); + }); + + it('should revert when input arrays have different lengths', async () => { + const amount1 = '100'; + const fee1 = '1'; + const fee2 = '2'; + + await makeTokenApproval(token1, payer, batch.address, getApprovalAmount([amount1], [fee1])); + await makeTokenApproval(token2, payer, batch.address, getApprovalAmount([amount1], [fee2])); + + const payee1Before = await balanceOf(token1, payee1); + const payee2Before = await balanceOf(token2, payee2); + + const { unchanged } = await expectRevertOrNoBalanceChange( + () => + batch.batchERC20PaymentsMultiTokensWithReference( + [token1.address, token2.address], + [payee1, payee2], + [amount1], + [REF_A, REF_B], + [fee1, fee2], + feeAddress, + { from: payer }, + ), + async () => [await balanceOf(token1, payee1), await balanceOf(token2, payee2)], + ); + + assert(unchanged, 'should not transfer when array lengths mismatch'); + assert.equal((await balanceOf(token1, payee1)).toString(), payee1Before.toString()); + assert.equal((await balanceOf(token2, payee2)).toString(), payee2Before.toString()); + }); + }); + + describe('batchEthPaymentsWithReference', () => { + it('should revert when calling batchEthPaymentsWithReference when EthFeeProxy is not set', async () => { + assert.equal(await batch.paymentEthFeeProxy(), TRON_ZERO_ADDRESS); + + const paymentAmount = String(10 * ONE_TRX_SUN); + const feeAmount = '0'; + const payeeBefore = await trxBalance(payee1); + const payerBefore = await trxBalance(payer); + + try { + await batch.batchEthPaymentsWithReference( + [payee1], + [paymentAmount], + [REF_A], + [feeAmount], + feeAddress, + { from: payer, callValue: Number(paymentAmount) }, + ); + } catch (_error) {} + await waitForConfirmation(2000); + + assert.equal((await trxBalance(payee1)).toString(), payeeBefore.toString()); + + const payerSpent = payerBefore - (await trxBalance(payer)); + assert( + payerSpent < BigInt(paymentAmount), + `payer should only spend tx fees, not ${paymentAmount} sun (spent ${payerSpent})`, + ); + }); + }); + }); + + describe('Admin', () => { + describe('setPaymentErc20FeeProxy', () => { + it('should allow owner to update proxy addresses', async () => { + const ERC20FeeProxy = artifacts.require('ERC20FeeProxy'); + const newProxy = await ERC20FeeProxy.new(); + await batch.setPaymentErc20FeeProxy(newProxy.address, { from: payer }); + assert.equal(await batch.paymentErc20FeeProxy(), newProxy.address); + }); + + it('should revert when a non-owner tries to update proxy addresses', async () => { + await expectNonOwnerReverts( + () => batch.setPaymentErc20FeeProxy(payee1, { from: payee1 }), + async () => await batch.paymentErc20FeeProxy(), + ); + }); + }); + + describe('setBatchFee', () => { + it('should allow owner to update the batch fee', async () => { + const newBatchFee = 50; + await batch.setBatchFee(newBatchFee, { from: payer }); + assert.equal((await batch.batchFee()).toString(), String(newBatchFee)); + await batch.setBatchFee(BATCH_FEE_BPS, { from: payer }); + }); + + it('should revert when a non-owner tries to set the batch fee', async () => { + await expectNonOwnerReverts( + () => batch.setBatchFee(99, { from: payee1 }), + async () => (await batch.batchFee()).toString(), + ); + }); + }); + + describe('setPaymentEthFeeProxy', () => { + it('should revert when a non-owner tries to set the EthFeeProxy address', async () => { + await expectNonOwnerReverts( + () => batch.setPaymentEthFeeProxy(TRON_ZERO_ADDRESS, { from: payee1 }), + async () => await batch.paymentEthFeeProxy(), + ); + }); + }); + }); +}); diff --git a/packages/smart-contracts/test/tron/ERC20BatchPayments.test.js b/packages/smart-contracts/test/tron/ERC20BatchPayments.test.js new file mode 100644 index 0000000000..0c1d8b900e --- /dev/null +++ b/packages/smart-contracts/test/tron/ERC20BatchPayments.test.js @@ -0,0 +1,772 @@ +const BatchPaymentsTronSimplified = artifacts.require('BatchPaymentsTronSimplified'); +const { + REF_A, + REF_B, + REF_C, + TRON_ZERO_ADDRESS, + waitForConfirmation, + balanceOf, + diff, + deployBaseSetup, + makeTokenApproval, + deployTokenWithSupply, + expectRevertOrNoBalanceChange, + assertBatchTokenBalancesZero, + expectNonOwnerReverts, + deployBadTRC20, + sumStrings, + mulString, + getApprovalAmount, +} = require('./helpers'); + +contract('BatchPaymentsTronSimplified Tron Test Suite', (accounts) => { + const payer = accounts[0]; + const payee1 = accounts[1] || 'TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE'; + const payee2 = accounts[2] || 'TG3XXyExBkPp9nzdajDZsozEu4BkaSJozs'; + const payee3 = accounts[3] || 'TFwt56qg984vEmk2UoDqUDeZhWEFSDaTmk'; + const feeAddress = accounts[4] || 'TNPGB28MjVCnEhTfpW51C2Ap3ZNnqGDXLB'; + + let batch; + let token1; + let token2; + let token3; + + before(async () => { + const setup = await deployBaseSetup({ + accounts, + batchDeployFn: (erc20FeeProxy, owner) => + BatchPaymentsTronSimplified.new(erc20FeeProxy.address, owner), + }); + batch = setup.batch; + [token1, token2, token3] = setup.tokens; + + console.log('\n=== BatchPaymentsTronSimplified Test Setup ==='); + console.log('Batch:', batch.address); + await waitForConfirmation(3000); + }); + + beforeEach(async () => { + await waitForConfirmation(2000); + }); + + describe('Happy Path Payment Scenarios', () => { + describe('batchERC20PaymentsWithReference', () => { + it('should pay 3 ERC20 payments', async () => { + const amount1 = '2000'; + const amount2 = '300'; + const amount3 = '400'; + const fee1 = '200'; + const fee2 = '20'; + const fee3 = '30'; + + await makeTokenApproval( + token1, + payer, + batch.address, + getApprovalAmount([amount1, amount2, amount3], [fee1, fee2, fee3]), + ); + + const payee1Before = await balanceOf(token1, payee1); + const payee2Before = await balanceOf(token1, payee2); + const feeBefore = await balanceOf(token1, feeAddress); + + await batch.batchERC20PaymentsWithReference( + token1.address, + [payee1, payee2, payee2], + [amount1, amount2, amount3], + [REF_A, REF_B, REF_C], + [fee1, fee2, fee3], + feeAddress, + { from: payer }, + ); + + assert.equal(diff(await balanceOf(token1, payee1), payee1Before).toString(), amount1); + assert.equal( + diff(await balanceOf(token1, payee2), payee2Before).toString(), + sumStrings([amount2, amount3]), + ); + assert.equal( + diff(await balanceOf(token1, feeAddress), feeBefore).toString(), + sumStrings([fee1, fee2, fee3]), + ); + }); + + it('should pay 10 ERC20 payments', async () => { + const amount = '200'; + const feeAmount = '100'; + const nbTxs = 10; + + await makeTokenApproval( + token1, + payer, + batch.address, + getApprovalAmount(Array(nbTxs).fill(amount), Array(nbTxs).fill(feeAmount)), + ); + + const payee1Before = await balanceOf(token1, payee1); + const feeBefore = await balanceOf(token1, feeAddress); + + await batch.batchERC20PaymentsWithReference( + token1.address, + Array(nbTxs).fill(payee1), + Array(nbTxs).fill(amount), + Array(nbTxs).fill(REF_A), + Array(nbTxs).fill(feeAmount), + feeAddress, + { from: payer }, + ); + + assert.equal( + diff(await balanceOf(token1, payee1), payee1Before).toString(), + mulString(amount, nbTxs), + ); + assert.equal( + diff(await balanceOf(token1, feeAddress), feeBefore).toString(), + mulString(feeAmount, nbTxs), + ); + }); + + it('should leave no token balance on the batch contract after a successful payment', async () => { + const amount1 = '100'; + const fee1 = '10'; + + await makeTokenApproval(token1, payer, batch.address, getApprovalAmount([amount1], [fee1])); + + await batch.batchERC20PaymentsWithReference( + token1.address, + [payee1], + [amount1], + [REF_A], + [fee1], + feeAddress, + { from: payer }, + ); + await waitForConfirmation(3000); + + await assertBatchTokenBalancesZero(batch, [token1]); + }); + + it('should pay a single ERC20 payment with BadTRC20', async () => { + const badToken = await deployBadTRC20(payer); + const paymentAmount = '100'; + const feeAmount = '10'; + + try { + await badToken.approve(batch.address, getApprovalAmount([paymentAmount], [feeAmount]), { + from: payer, + }); + await waitForConfirmation(3000); + + const payeeBefore = await balanceOf(badToken, payee1); + + await batch.batchERC20PaymentsWithReference( + badToken.address, + [payee1], + [paymentAmount], + [REF_A], + [feeAmount], + feeAddress, + { from: payer }, + ); + await waitForConfirmation(3000); + + const payeeAfter = await balanceOf(badToken, payee1); + assert( + payeeAfter > payeeBefore, + 'BadTRC20: payee balance should increase when batch payment succeeds', + ); + } catch (_error) { + console.log( + 'BadTRC20 batch payment rejected by Tron (acceptable for non-standard tokens)', + ); + } + }); + }); + + describe('batchERC20PaymentsMultiTokensWithReference', () => { + it('should pay 3 ERC20 payments in three different tokens', async () => { + const amount1 = '5000'; + const amount2 = '3000'; + const amount3 = '4000'; + const fee1 = '600'; + const fee2 = '200'; + const fee3 = '300'; + + await makeTokenApproval(token1, payer, batch.address, getApprovalAmount([amount1], [fee1])); + await makeTokenApproval(token2, payer, batch.address, getApprovalAmount([amount2], [fee2])); + await makeTokenApproval(token3, payer, batch.address, getApprovalAmount([amount3], [fee3])); + + const payee1Before = await balanceOf(token1, payee1); + const payee2Token2Before = await balanceOf(token2, payee2); + const payee2Token3Before = await balanceOf(token3, payee2); + const feeToken1Before = await balanceOf(token1, feeAddress); + const feeToken2Before = await balanceOf(token2, feeAddress); + const feeToken3Before = await balanceOf(token3, feeAddress); + + await batch.batchERC20PaymentsMultiTokensWithReference( + [token1.address, token2.address, token3.address], + [payee1, payee2, payee2], + [amount1, amount2, amount3], + [REF_A, REF_B, REF_C], + [fee1, fee2, fee3], + feeAddress, + { from: payer }, + ); + + assert.equal(diff(await balanceOf(token1, payee1), payee1Before).toString(), amount1); + assert.equal(diff(await balanceOf(token2, payee2), payee2Token2Before).toString(), amount2); + assert.equal(diff(await balanceOf(token3, payee2), payee2Token3Before).toString(), amount3); + assert.equal(diff(await balanceOf(token1, feeAddress), feeToken1Before).toString(), fee1); + assert.equal(diff(await balanceOf(token2, feeAddress), feeToken2Before).toString(), fee2); + assert.equal(diff(await balanceOf(token3, feeAddress), feeToken3Before).toString(), fee3); + }); + + it('should pay 3 ERC20 payments in three different tokens with a zero amount payment', async () => { + const amount1 = '5000'; + const amount2 = '0'; + const amount3 = '4000'; + const fee1 = '600'; + const fee2 = '0'; + const fee3 = '300'; + + await makeTokenApproval(token1, payer, batch.address, getApprovalAmount([amount1], [fee1])); + await makeTokenApproval(token2, payer, batch.address, getApprovalAmount([amount2], [fee2])); + await makeTokenApproval(token3, payer, batch.address, getApprovalAmount([amount3], [fee3])); + + const payee1Before = await balanceOf(token1, payee1); + const payee2Token2Before = await balanceOf(token2, payee2); + const payee2Token3Before = await balanceOf(token3, payee2); + + await batch.batchERC20PaymentsMultiTokensWithReference( + [token1.address, token2.address, token3.address], + [payee1, payee2, payee2], + [amount1, amount2, amount3], + [REF_A, REF_B, REF_C], + [fee1, fee2, fee3], + feeAddress, + { from: payer }, + ); + + assert.equal(diff(await balanceOf(token1, payee1), payee1Before).toString(), amount1); + assert.equal(diff(await balanceOf(token2, payee2), payee2Token2Before).toString(), amount2); + assert.equal(diff(await balanceOf(token3, payee2), payee2Token3Before).toString(), amount3); + }); + + it('should pay 4 ERC20 payments in two different tokens', async () => { + const amount1 = '200'; + const amount2 = '200'; + const amount3 = '200'; + const amount4 = '200'; + const fee1 = '10'; + const fee2 = '10'; + const fee3 = '10'; + const fee4 = '10'; + + await makeTokenApproval( + token1, + payer, + batch.address, + getApprovalAmount([amount1, amount2], [fee1, fee2]), + ); + await makeTokenApproval( + token2, + payer, + batch.address, + getApprovalAmount([amount3, amount4], [fee3, fee4]), + ); + + const payee2Token1Before = await balanceOf(token1, payee2); + const payee2Token2Before = await balanceOf(token2, payee2); + + await batch.batchERC20PaymentsMultiTokensWithReference( + [token1.address, token1.address, token2.address, token2.address], + [payee2, payee2, payee2, payee2], + [amount1, amount2, amount3, amount4], + [REF_A, REF_A, REF_A, REF_A], + [fee1, fee2, fee3, fee4], + feeAddress, + { from: payer }, + ); + + assert.equal( + diff(await balanceOf(token1, payee2), payee2Token1Before).toString(), + sumStrings([amount1, amount2]), + ); + assert.equal( + diff(await balanceOf(token2, payee2), payee2Token2Before).toString(), + sumStrings([amount3, amount4]), + ); + }); + + it('should pay 10 ERC20 payments in two different tokens', async () => { + const amount = '20'; + const feeAmount = '10'; + const nbPaymentsPerToken = 5; + + await makeTokenApproval( + token1, + payer, + batch.address, + getApprovalAmount( + Array(nbPaymentsPerToken).fill(amount), + Array(nbPaymentsPerToken).fill(feeAmount), + ), + ); + await makeTokenApproval( + token2, + payer, + batch.address, + getApprovalAmount( + Array(nbPaymentsPerToken).fill(amount), + Array(nbPaymentsPerToken).fill(feeAmount), + ), + ); + + const payee1Token1Before = await balanceOf(token1, payee1); + const payee1Token2Before = await balanceOf(token2, payee1); + + await batch.batchERC20PaymentsMultiTokensWithReference( + [ + ...Array(nbPaymentsPerToken).fill(token1.address), + ...Array(nbPaymentsPerToken).fill(token2.address), + ], + Array(nbPaymentsPerToken * 2).fill(payee1), + Array(nbPaymentsPerToken * 2).fill(amount), + Array(nbPaymentsPerToken * 2).fill(REF_A), + Array(nbPaymentsPerToken * 2).fill(feeAmount), + feeAddress, + { from: payer }, + ); + + assert.equal( + diff(await balanceOf(token1, payee1), payee1Token1Before).toString(), + mulString(amount, nbPaymentsPerToken), + ); + assert.equal( + diff(await balanceOf(token2, payee1), payee1Token2Before).toString(), + mulString(amount, nbPaymentsPerToken), + ); + }); + + it('should pay 10 ERC20 payments in two different tokens without fees', async () => { + const amount = '20'; + const feeAmount = '0'; + const nbPaymentsPerToken = 5; + + await makeTokenApproval( + token1, + payer, + batch.address, + getApprovalAmount( + Array(nbPaymentsPerToken).fill(amount), + Array(nbPaymentsPerToken).fill(feeAmount), + ), + ); + await makeTokenApproval( + token2, + payer, + batch.address, + getApprovalAmount( + Array(nbPaymentsPerToken).fill(amount), + Array(nbPaymentsPerToken).fill(feeAmount), + ), + ); + + const payee1Token1Before = await balanceOf(token1, payee1); + const payee1Token2Before = await balanceOf(token2, payee1); + const feeToken1Before = await balanceOf(token1, feeAddress); + const feeToken2Before = await balanceOf(token2, feeAddress); + + await batch.batchERC20PaymentsMultiTokensWithReference( + [ + ...Array(nbPaymentsPerToken).fill(token1.address), + ...Array(nbPaymentsPerToken).fill(token2.address), + ], + Array(nbPaymentsPerToken * 2).fill(payee1), + Array(nbPaymentsPerToken * 2).fill(amount), + Array(nbPaymentsPerToken * 2).fill(REF_A), + Array(nbPaymentsPerToken * 2).fill(feeAmount), + TRON_ZERO_ADDRESS, + { from: payer }, + ); + await waitForConfirmation(3000); + + assert.equal( + diff(await balanceOf(token1, payee1), payee1Token1Before).toString(), + mulString(amount, nbPaymentsPerToken), + ); + assert.equal( + diff(await balanceOf(token2, payee1), payee1Token2Before).toString(), + mulString(amount, nbPaymentsPerToken), + ); + assert.equal( + diff(await balanceOf(token1, feeAddress), feeToken1Before).toString(), + feeAmount, + ); + assert.equal( + diff(await balanceOf(token2, feeAddress), feeToken2Before).toString(), + feeAmount, + ); + }); + + it('should leave no token balance on the batch contract after a successful payment', async () => { + const amount1 = '100'; + const amount2 = '200'; + const fee1 = '10'; + const fee2 = '20'; + + await makeTokenApproval(token1, payer, batch.address, getApprovalAmount([amount1], [fee1])); + await makeTokenApproval(token2, payer, batch.address, getApprovalAmount([amount2], [fee2])); + + await batch.batchERC20PaymentsMultiTokensWithReference( + [token1.address, token2.address], + [payee1, payee2], + [amount1, amount2], + [REF_A, REF_B], + [fee1, fee2], + feeAddress, + { from: payer }, + ); + await waitForConfirmation(3000); + + await assertBatchTokenBalancesZero(batch, [token1, token2]); + }); + + it('should pay a multi-token ERC20 payment with BadTRC20', async () => { + const badToken = await deployBadTRC20(payer); + const paymentAmount = '100'; + const feeAmount = '10'; + const amount1 = '50'; + const fee1 = '5'; + + try { + await badToken.approve(batch.address, getApprovalAmount([paymentAmount], [feeAmount]), { + from: payer, + }); + await makeTokenApproval( + token1, + payer, + batch.address, + getApprovalAmount([amount1], [fee1]), + ); + await waitForConfirmation(3000); + + const badPayeeBefore = await balanceOf(badToken, payee1); + const payee1Before = await balanceOf(token1, payee2); + + await batch.batchERC20PaymentsMultiTokensWithReference( + [badToken.address, token1.address], + [payee1, payee2], + [paymentAmount, amount1], + [REF_A, REF_B], + [feeAmount, fee1], + feeAddress, + { from: payer }, + ); + await waitForConfirmation(3000); + + const badPayeeAfter = await balanceOf(badToken, payee1); + const payee1After = await balanceOf(token1, payee2); + assert( + badPayeeAfter > badPayeeBefore || payee1After > payee1Before, + 'BadTRC20 multi-token: at least one payee balance should increase when batch succeeds', + ); + } catch (_error) { + console.log( + 'BadTRC20 multi-token batch payment rejected by Tron (acceptable for non-standard tokens)', + ); + } + }); + }); + }); + + describe('Error cases scenarios', () => { + describe('batchERC20PaymentsWithReference', () => { + it('should revert when the payer does not have enough funds to pay', async () => { + const amount1 = '5'; + const amount2 = '30'; + const amount3 = '400'; + const fee1 = '1'; + const fee2 = '2'; + const fee3 = '3'; + + const lowToken = await deployTokenWithSupply('100', payer); + await makeTokenApproval( + lowToken, + payer, + batch.address, + getApprovalAmount([amount1, amount2, amount3], [fee1, fee2, fee3]), + ); + + const payee3Before = await balanceOf(lowToken, payee3); + const { unchanged } = await expectRevertOrNoBalanceChange( + () => + batch.batchERC20PaymentsWithReference( + lowToken.address, + [payee1, payee2, payee3], + [amount1, amount2, amount3], + [REF_A, REF_B, REF_C], + [fee1, fee2, fee3], + feeAddress, + { from: payer }, + ), + async () => [await balanceOf(lowToken, payee3)], + ); + + assert(unchanged, 'should not transfer when funds insufficient'); + assert.equal((await balanceOf(lowToken, payee3)).toString(), payee3Before.toString()); + }); + + it('should revert when the payer does not have enough funds to pay the fees', async () => { + const amount1 = '100'; + const amount2 = '200'; + const fee1 = '50'; + const fee2 = '50'; + + const lowToken = await deployTokenWithSupply('300', payer); + await makeTokenApproval( + lowToken, + payer, + batch.address, + getApprovalAmount([amount1, amount2], [fee1, fee2]), + ); + + const payee1Before = await balanceOf(lowToken, payee1); + const { unchanged } = await expectRevertOrNoBalanceChange( + () => + batch.batchERC20PaymentsWithReference( + lowToken.address, + [payee1, payee2], + [amount1, amount2], + [REF_A, REF_B], + [fee1, fee2], + feeAddress, + { from: payer }, + ), + async () => [await balanceOf(lowToken, payee1)], + ); + + assert(unchanged, 'should not transfer when fees cannot be paid'); + assert.equal((await balanceOf(lowToken, payee1)).toString(), payee1Before.toString()); + }); + + it('should revert when the payer did not approve the batch contract to spend the tokens', async () => { + const amount1 = '20'; + const amount2 = '30'; + const amount3 = '40'; + const fee1 = '1'; + const fee2 = '2'; + const fee3 = '3'; + + await makeTokenApproval( + token1, + payer, + batch.address, + getApprovalAmount([amount1, amount2, amount3], [fee1, fee2, fee3]), + ); + await token1.approve(batch.address, '10', { from: payer }); + await waitForConfirmation(2000); + + const payee1Before = await balanceOf(token1, payee1); + const { unchanged } = await expectRevertOrNoBalanceChange( + () => + batch.batchERC20PaymentsWithReference( + token1.address, + [payee1, payee2, payee3], + [amount1, amount2, amount3], + [REF_A, REF_B, REF_C], + [fee1, fee2, fee3], + feeAddress, + { from: payer }, + ), + async () => [await balanceOf(token1, payee1)], + ); + + assert(unchanged, 'should not transfer without allowance'); + assert.equal((await balanceOf(token1, payee1)).toString(), payee1Before.toString()); + }); + + it('should revert when input arrays have different lengths', async () => { + const amount1 = '100'; + const fee1 = '1'; + const fee2 = '2'; + + await makeTokenApproval( + token1, + payer, + batch.address, + getApprovalAmount([amount1], [fee1, fee2]), + ); + + const payee1Before = await balanceOf(token1, payee1); + const payee2Before = await balanceOf(token1, payee2); + + const { unchanged } = await expectRevertOrNoBalanceChange( + () => + batch.batchERC20PaymentsWithReference( + token1.address, + [payee1, payee2], + [amount1], + [REF_A, REF_B], + [fee1, fee2], + feeAddress, + { from: payer }, + ), + async () => [await balanceOf(token1, payee1), await balanceOf(token1, payee2)], + ); + + assert(unchanged, 'should not transfer when array lengths mismatch'); + assert.equal((await balanceOf(token1, payee1)).toString(), payee1Before.toString()); + assert.equal((await balanceOf(token1, payee2)).toString(), payee2Before.toString()); + }); + }); + + describe('batchERC20PaymentsMultiTokensWithReference', () => { + it('should revert when the payer does not have enough funds to pay in at least one of the tokens', async () => { + const amount1 = '5'; + const amount2 = '30'; + const amount3 = '400'; + const fee1 = '1'; + const fee2 = '2'; + const fee3 = '3'; + + const lowToken = await deployTokenWithSupply('400', payer); + await makeTokenApproval( + lowToken, + payer, + batch.address, + getApprovalAmount([amount1, amount2, amount3], [fee1, fee2, fee3]), + ); + + const payee3Before = await balanceOf(lowToken, payee3); + const { unchanged } = await expectRevertOrNoBalanceChange( + () => + batch.batchERC20PaymentsMultiTokensWithReference( + [lowToken.address, lowToken.address, lowToken.address], + [payee1, payee2, payee3], + [amount1, amount2, amount3], + [REF_A, REF_B, REF_C], + [fee1, fee2, fee3], + feeAddress, + { from: payer }, + ), + async () => [await balanceOf(lowToken, payee3)], + ); + + assert(unchanged, 'multi-token batch should not transfer when funds insufficient'); + assert.equal((await balanceOf(lowToken, payee3)).toString(), payee3Before.toString()); + }); + + it('should revert when the payer did not approve the batch contract to spend the tokens in at least one of the tokens', async () => { + const amount1 = '100'; + const amount2 = '200'; + const amount3 = '300'; + const fee1 = '1'; + const fee2 = '2'; + const fee3 = '3'; + + await makeTokenApproval( + token1, + payer, + batch.address, + getApprovalAmount([amount1, amount2, amount3], [fee1, fee2, fee3]), + ); + await token1.approve(batch.address, '10', { from: payer }); + await waitForConfirmation(2000); + + const payee1Before = await balanceOf(token1, payee1); + const { unchanged } = await expectRevertOrNoBalanceChange( + () => + batch.batchERC20PaymentsMultiTokensWithReference( + [token1.address, token1.address, token1.address], + [payee1, payee2, payee3], + [amount1, amount2, amount3], + [REF_A, REF_B, REF_C], + [fee1, fee2, fee3], + feeAddress, + { from: payer }, + ), + async () => [await balanceOf(token1, payee1)], + ); + + assert(unchanged, 'multi-token batch should not transfer without allowance'); + assert.equal((await balanceOf(token1, payee1)).toString(), payee1Before.toString()); + }); + + it('should revert when the payer did not approve the batch contract for one of the tokens', async () => { + const amount1 = '100'; + const amount2 = '200'; + const fee1 = '1'; + const fee2 = '2'; + + await makeTokenApproval(token1, payer, batch.address, getApprovalAmount([amount1], [fee1])); + + const payee2Token2Before = await balanceOf(token2, payee2); + const { unchanged } = await expectRevertOrNoBalanceChange( + () => + batch.batchERC20PaymentsMultiTokensWithReference( + [token1.address, token2.address], + [payee1, payee2], + [amount1, amount2], + [REF_A, REF_B], + [fee1, fee2], + feeAddress, + { from: payer }, + ), + async () => [await balanceOf(token2, payee2)], + ); + + assert(unchanged, 'should not transfer when one token lacks approval'); + assert.equal((await balanceOf(token2, payee2)).toString(), payee2Token2Before.toString()); + }); + + it('should revert when input arrays have different lengths', async () => { + const amount1 = '100'; + const fee1 = '1'; + const fee2 = '2'; + + await makeTokenApproval(token1, payer, batch.address, getApprovalAmount([amount1], [fee1])); + await makeTokenApproval(token2, payer, batch.address, getApprovalAmount([amount1], [fee2])); + + const payee1Before = await balanceOf(token1, payee1); + const payee2Before = await balanceOf(token2, payee2); + + const { unchanged } = await expectRevertOrNoBalanceChange( + () => + batch.batchERC20PaymentsMultiTokensWithReference( + [token1.address, token2.address], + [payee1, payee2], + [amount1], + [REF_A, REF_B], + [fee1, fee2], + feeAddress, + { from: payer }, + ), + async () => [await balanceOf(token1, payee1), await balanceOf(token2, payee2)], + ); + + assert(unchanged, 'should not transfer when array lengths mismatch'); + assert.equal((await balanceOf(token1, payee1)).toString(), payee1Before.toString()); + assert.equal((await balanceOf(token2, payee2)).toString(), payee2Before.toString()); + }); + }); + }); + + describe('Admin', () => { + describe('setPaymentErc20FeeProxy', () => { + it('should allow owner to update proxy addresses', async () => { + const ERC20FeeProxy = artifacts.require('ERC20FeeProxy'); + const newProxy = await ERC20FeeProxy.new(); + await batch.setPaymentErc20FeeProxy(newProxy.address, { from: payer }); + assert.equal(await batch.paymentErc20FeeProxy(), newProxy.address); + }); + + it('should revert when a non-owner tries to update proxy addresses', async () => { + await expectNonOwnerReverts( + () => batch.setPaymentErc20FeeProxy(payee1, { from: payee1 }), + async () => await batch.paymentErc20FeeProxy(), + ); + }); + }); + }); +}); diff --git a/packages/smart-contracts/test/tron/helpers.js b/packages/smart-contracts/test/tron/helpers.js new file mode 100644 index 0000000000..9efc5959e2 --- /dev/null +++ b/packages/smart-contracts/test/tron/helpers.js @@ -0,0 +1,155 @@ +const INITIAL_SUPPLY = '10000000000'; + +const REF_A = '0xaaaa'; +const REF_B = '0xbbbb'; +const REF_C = '0xcccc'; + +/** Tron base58 zero address (unset EthFeeProxy on Tron deployments). */ +const TRON_ZERO_ADDRESS = '410000000000000000000000000000000000000000'; + +/** 1 TRX = 1_000_000 sun on Tron. */ +const ONE_TRX_SUN = 1_000_000; + +const waitForConfirmation = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +const balanceOf = async (token, account) => { + const value = await token.balanceOf(account); + return BigInt(value.toString()); +}; + +const trxBalance = async (address) => { + const balance = await tronWeb.trx.getBalance(address); + return BigInt(balance); +}; + +const diff = (after, before) => after - before; + +const sumStrings = (values) => values.reduce((acc, value) => acc + BigInt(value), 0n).toString(); + +const mulString = (value, count) => (BigInt(value) * BigInt(count)).toString(); + +const computeBatchFee = (totalPaymentAmount, bps) => + ((BigInt(totalPaymentAmount) * BigInt(bps)) / 1000n).toString(); + +const getApprovalAmount = (amountList, feeList, batchFee = '0') => + sumStrings([...amountList, ...feeList, batchFee]); + +/** + * Deploy ERC20FeeProxy, optional batch contract, and one or more TestTRC20 tokens. + */ +const deployBaseSetup = async ({ accounts, batchDeployFn, batchFee, tokenCount = 3 }) => { + const ERC20FeeProxy = artifacts.require('ERC20FeeProxy'); + const TestTRC20 = artifacts.require('TestTRC20'); + + const owner = accounts[0]; + const erc20FeeProxy = await ERC20FeeProxy.new(); + const dummyEthProxy = TRON_ZERO_ADDRESS; + + let batch = null; + if (batchDeployFn) { + batch = await batchDeployFn(erc20FeeProxy, owner, dummyEthProxy); + if (batchFee !== undefined && batch.setBatchFee) { + await batch.setBatchFee(batchFee, { from: owner }); + } + } + + const tokens = []; + for (let i = 0; i < tokenCount; i++) { + const token = await TestTRC20.new(INITIAL_SUPPLY, `Test TRC20 ${i + 1}`, `TT${i + 1}`, 18); + tokens.push(token); + } + + return { erc20FeeProxy, batch, tokens, dummyEthProxy }; +}; + +/** + * Approve contract to spend payer tokens. + */ +const makeTokenApproval = async (token, payer, batchAddress, amount) => { + await token.approve(batchAddress, amount, { from: payer }); + await waitForConfirmation(2000); +}; + +/** + * Deploy a TestTRC20 with a specific initial supply assigned to payer. + */ +const deployTokenWithSupply = async (supply, payer) => { + const TestTRC20 = artifacts.require('TestTRC20'); + return TestTRC20.new(supply, 'Test TRC20', 'TTRC', 18, { from: payer }); +}; + +/** + * Runs fn and asserts tracked balances are unchanged (source of truth Tron when Tron tx reverts). + */ +const expectRevertOrNoBalanceChange = async (fn, getBalances) => { + const before = await getBalances(); + try { + await fn(); + } catch (_error) {} + await waitForConfirmation(2000); + const after = await getBalances(); + const unchanged = before.every((value, index) => value === after[index]); + return { unchanged }; +}; + +/** + * Asserts the batch contract holds zero balance for each token. + */ +const assertBatchTokenBalancesZero = async (batch, tokens) => { + for (const token of tokens) { + const bal = await balanceOf(token, batch.address); + assert.equal(bal.toString(), '0', `batch should have zero token balance for ${token.address}`); + } +}; + +/** + * Expects fn to revert; optionally asserts getState() is unchanged. + */ +const expectNonOwnerReverts = async (fn, getState) => { + const before = await getState(); + let threw = false; + try { + await fn(); + } catch (_error) { + threw = true; + } + await waitForConfirmation(2000); + assert(threw, 'expected non-owner call to revert'); + if (getState) { + const after = await getState(); + assert.equal(after, before, 'state should be unchanged after failed non-owner call'); + } + return { reverted: threw }; +}; + +/** + * Deploy BadTRC20 with migration-style constructor args. + */ +const deployBadTRC20 = async (payer) => { + const BadTRC20 = artifacts.require('BadTRC20'); + return BadTRC20.new('1000000000000', 'BadTRC20', 'BAD', 8, { from: payer }); +}; + +module.exports = { + INITIAL_SUPPLY, + REF_A, + REF_B, + REF_C, + TRON_ZERO_ADDRESS, + ONE_TRX_SUN, + waitForConfirmation, + balanceOf, + trxBalance, + diff, + sumStrings, + mulString, + computeBatchFee, + getApprovalAmount, + deployBaseSetup, + makeTokenApproval, + deployTokenWithSupply, + expectRevertOrNoBalanceChange, + assertBatchTokenBalancesZero, + expectNonOwnerReverts, + deployBadTRC20, +}; diff --git a/packages/smart-contracts/tron/contracts/BatchPayments.sol b/packages/smart-contracts/tron/contracts/BatchPayments.sol new file mode 120000 index 0000000000..5f16a2c12b --- /dev/null +++ b/packages/smart-contracts/tron/contracts/BatchPayments.sol @@ -0,0 +1 @@ +../../src/contracts/BatchPayments.sol \ No newline at end of file diff --git a/packages/smart-contracts/tron/contracts/ERC20BatchPayments.sol b/packages/smart-contracts/tron/contracts/ERC20BatchPayments.sol new file mode 100644 index 0000000000..e8275cc2f5 --- /dev/null +++ b/packages/smart-contracts/tron/contracts/ERC20BatchPayments.sol @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import '@openzeppelin/contracts/access/Ownable.sol'; +import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import './interfaces/ERC20FeeProxy.sol'; +import './lib/SafeERC20.sol'; + +/** + * @title BatchPaymentsTronSimplified + * @notice Tron-only batch contract that routes each payment through ERC20FeeProxy. + * If one payment fails, the whole batch reverts. + * @dev Uses ERC20FeeProxy to pay an invoice and fees, with a payment reference. + * Make sure this contract has allowance to spend the payer's tokens. + * Make sure the payer has enough tokens to pay the amounts and fees. + */ +contract BatchPaymentsTronSimplified is Ownable { + using SafeERC20 for IERC20; + + IERC20FeeProxy public paymentErc20FeeProxy; + + struct Token { + address tokenAddress; + uint256 amountAndFee; + } + + /** + * @param _paymentErc20FeeProxy The address of the ERC20FeeProxy to use. + * @param _owner Owner of the contract. + */ + constructor(address _paymentErc20FeeProxy, address _owner) { + paymentErc20FeeProxy = IERC20FeeProxy(_paymentErc20FeeProxy); + transferOwnership(_owner); + } + + /** + * @notice Send a batch of ERC20 payments with fees and payment references to multiple accounts. + * @param _tokenAddress Token to transact with. + * @param _recipients List of recipient accounts. + * @param _amounts List of amounts, corresponding to recipients[]. + * @param _paymentReferences List of payment references, corresponding to recipients[]. + * @param _feeAmounts List of fee amounts, corresponding to recipients[]. + * @param _feeAddress The fee recipient. + */ + function batchERC20PaymentsWithReference( + address _tokenAddress, + address[] calldata _recipients, + uint256[] calldata _amounts, + bytes[] calldata _paymentReferences, + uint256[] calldata _feeAmounts, + address _feeAddress + ) external { + require( + _recipients.length == _amounts.length && + _recipients.length == _paymentReferences.length && + _recipients.length == _feeAmounts.length, + 'the input arrays must have the same length' + ); + + uint256 amountAndFee = 0; + for (uint256 i = 0; i < _recipients.length; i++) { + amountAndFee += _amounts[i] + _feeAmounts[i]; + } + + _transferToContractAndApproveProxy(IERC20(_tokenAddress), amountAndFee); + + for (uint256 i = 0; i < _recipients.length; i++) { + paymentErc20FeeProxy.transferFromWithReferenceAndFee( + _tokenAddress, + _recipients[i], + _amounts[i], + _paymentReferences[i], + _feeAmounts[i], + _feeAddress + ); + } + } + + /** + * @notice Send a batch of ERC20 payments on multiple tokens with fees and payment references. + * @param _tokenAddresses List of tokens to transact with. + * @param _recipients List of recipient accounts. + * @param _amounts List of amounts, corresponding to recipients[]. + * @param _paymentReferences List of payment references, corresponding to recipients[]. + * @param _feeAmounts List of fee amounts, corresponding to recipients[]. + * @param _feeAddress The fee recipient. + */ + function batchERC20PaymentsMultiTokensWithReference( + address[] calldata _tokenAddresses, + address[] calldata _recipients, + uint256[] calldata _amounts, + bytes[] calldata _paymentReferences, + uint256[] calldata _feeAmounts, + address _feeAddress + ) external { + require( + _tokenAddresses.length == _recipients.length && + _tokenAddresses.length == _amounts.length && + _tokenAddresses.length == _paymentReferences.length && + _tokenAddresses.length == _feeAmounts.length, + 'the input arrays must have the same length' + ); + + Token[] memory uniqueTokens = new Token[](_tokenAddresses.length); + for (uint256 i = 0; i < _tokenAddresses.length; i++) { + for (uint256 j = 0; j < _tokenAddresses.length; j++) { + if (uniqueTokens[j].tokenAddress == _tokenAddresses[i]) { + uniqueTokens[j].amountAndFee += _amounts[i] + _feeAmounts[i]; + break; + } + if (uniqueTokens[j].amountAndFee == 0 && (_amounts[i] + _feeAmounts[i]) > 0) { + uniqueTokens[j].tokenAddress = _tokenAddresses[i]; + uniqueTokens[j].amountAndFee = _amounts[i] + _feeAmounts[i]; + break; + } + } + } + + for (uint256 i = 0; i < uniqueTokens.length && uniqueTokens[i].amountAndFee > 0; i++) { + _transferToContractAndApproveProxy( + IERC20(uniqueTokens[i].tokenAddress), + uniqueTokens[i].amountAndFee + ); + } + + for (uint256 i = 0; i < _recipients.length; i++) { + paymentErc20FeeProxy.transferFromWithReferenceAndFee( + _tokenAddresses[i], + _recipients[i], + _amounts[i], + _paymentReferences[i], + _feeAmounts[i], + _feeAddress + ); + } + } + + /** + * @notice Authorizes the proxy to spend a request currency (ERC20). + * @param _erc20Address Address of an ERC20 used as the request currency. + */ + function approvePaymentProxyToSpend(address _erc20Address) public { + IERC20 erc20 = IERC20(_erc20Address); + uint256 max = type(uint256).max; + require(erc20.safeApprove(address(paymentErc20FeeProxy), max), 'approve() failed'); + } + + /** + * @notice Updates the ERC20FeeProxy address. + * @param _paymentErc20FeeProxy The address of the ERC20FeeProxy to use. + */ + function setPaymentErc20FeeProxy(address _paymentErc20FeeProxy) public onlyOwner { + paymentErc20FeeProxy = IERC20FeeProxy(_paymentErc20FeeProxy); + } + + /** + * @notice Pulls tokens from the payer to this contract and approves the proxy to spend them. + * @param requestedToken The token to pay. + * @param amountAndFee The sum of payment amounts and fees for this token. + */ + function _transferToContractAndApproveProxy( + IERC20 requestedToken, + uint256 amountAndFee + ) internal { + require( + requestedToken.allowance(msg.sender, address(this)) >= amountAndFee, + 'Not sufficient allowance for batch to pay' + ); + require(requestedToken.balanceOf(msg.sender) >= amountAndFee, 'not enough funds'); + require( + requestedToken.safeTransferFrom(msg.sender, address(this), amountAndFee), + 'payment transferFrom() failed' + ); + + if (requestedToken.allowance(address(this), address(paymentErc20FeeProxy)) < amountAndFee) { + approvePaymentProxyToSpend(address(requestedToken)); + } + } +} diff --git a/packages/smart-contracts/tron/contracts/interfaces/ERC20FeeProxy.sol b/packages/smart-contracts/tron/contracts/interfaces/ERC20FeeProxy.sol new file mode 120000 index 0000000000..88ec30138c --- /dev/null +++ b/packages/smart-contracts/tron/contracts/interfaces/ERC20FeeProxy.sol @@ -0,0 +1 @@ +../../../src/contracts/interfaces/ERC20FeeProxy.sol \ No newline at end of file diff --git a/packages/smart-contracts/tron/contracts/interfaces/EthereumFeeProxy.sol b/packages/smart-contracts/tron/contracts/interfaces/EthereumFeeProxy.sol new file mode 120000 index 0000000000..2ba444a117 --- /dev/null +++ b/packages/smart-contracts/tron/contracts/interfaces/EthereumFeeProxy.sol @@ -0,0 +1 @@ +../../../src/contracts/interfaces/EthereumFeeProxy.sol \ No newline at end of file diff --git a/packages/smart-contracts/tron/contracts/lib/SafeERC20.sol b/packages/smart-contracts/tron/contracts/lib/SafeERC20.sol new file mode 120000 index 0000000000..4003968ec2 --- /dev/null +++ b/packages/smart-contracts/tron/contracts/lib/SafeERC20.sol @@ -0,0 +1 @@ +../../../src/contracts/lib/SafeERC20.sol \ No newline at end of file From d356b3c34b4ec3fad7fadba24e6c7bc330a8e354 Mon Sep 17 00:00:00 2001 From: LeoSlrRf Date: Wed, 20 May 2026 13:02:52 +0200 Subject: [PATCH 02/11] fix: contract name --- packages/smart-contracts/test/tron/ERC20BatchPayments.test.js | 2 +- .../smart-contracts/tron/contracts/ERC20BatchPayments.sol | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/smart-contracts/test/tron/ERC20BatchPayments.test.js b/packages/smart-contracts/test/tron/ERC20BatchPayments.test.js index 0c1d8b900e..43c3709698 100644 --- a/packages/smart-contracts/test/tron/ERC20BatchPayments.test.js +++ b/packages/smart-contracts/test/tron/ERC20BatchPayments.test.js @@ -19,7 +19,7 @@ const { getApprovalAmount, } = require('./helpers'); -contract('BatchPaymentsTronSimplified Tron Test Suite', (accounts) => { +contract('ERC20BatchPayments Tron Test Suite', (accounts) => { const payer = accounts[0]; const payee1 = accounts[1] || 'TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE'; const payee2 = accounts[2] || 'TG3XXyExBkPp9nzdajDZsozEu4BkaSJozs'; diff --git a/packages/smart-contracts/tron/contracts/ERC20BatchPayments.sol b/packages/smart-contracts/tron/contracts/ERC20BatchPayments.sol index e8275cc2f5..0d1b85fcba 100644 --- a/packages/smart-contracts/tron/contracts/ERC20BatchPayments.sol +++ b/packages/smart-contracts/tron/contracts/ERC20BatchPayments.sol @@ -7,14 +7,14 @@ import './interfaces/ERC20FeeProxy.sol'; import './lib/SafeERC20.sol'; /** - * @title BatchPaymentsTronSimplified + * @title ERC20BatchPayments * @notice Tron-only batch contract that routes each payment through ERC20FeeProxy. * If one payment fails, the whole batch reverts. * @dev Uses ERC20FeeProxy to pay an invoice and fees, with a payment reference. * Make sure this contract has allowance to spend the payer's tokens. * Make sure the payer has enough tokens to pay the amounts and fees. */ -contract BatchPaymentsTronSimplified is Ownable { +contract ERC20BatchPayments is Ownable { using SafeERC20 for IERC20; IERC20FeeProxy public paymentErc20FeeProxy; From 2d355772a2315b1c0cf42fcb3b7d6739b11b09f6 Mon Sep 17 00:00:00 2001 From: LeoSlrRf Date: Wed, 20 May 2026 13:36:30 +0200 Subject: [PATCH 03/11] fix: imports & naming --- .../smart-contracts/test/tron/ERC20BatchPayments.test.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/smart-contracts/test/tron/ERC20BatchPayments.test.js b/packages/smart-contracts/test/tron/ERC20BatchPayments.test.js index 43c3709698..866a6d20a3 100644 --- a/packages/smart-contracts/test/tron/ERC20BatchPayments.test.js +++ b/packages/smart-contracts/test/tron/ERC20BatchPayments.test.js @@ -1,4 +1,4 @@ -const BatchPaymentsTronSimplified = artifacts.require('BatchPaymentsTronSimplified'); +const ERC20BatchPayments = artifacts.require('ERC20BatchPayments'); const { REF_A, REF_B, @@ -34,13 +34,12 @@ contract('ERC20BatchPayments Tron Test Suite', (accounts) => { before(async () => { const setup = await deployBaseSetup({ accounts, - batchDeployFn: (erc20FeeProxy, owner) => - BatchPaymentsTronSimplified.new(erc20FeeProxy.address, owner), + batchDeployFn: (erc20FeeProxy, owner) => ERC20BatchPayments.new(erc20FeeProxy.address, owner), }); batch = setup.batch; [token1, token2, token3] = setup.tokens; - console.log('\n=== BatchPaymentsTronSimplified Test Setup ==='); + console.log('\n=== ERC20BatchPayments Test Setup ==='); console.log('Batch:', batch.address); await waitForConfirmation(3000); }); From 93e29c9710c08c1b5e031b89fb51c157c3fae1aa Mon Sep 17 00:00:00 2001 From: LeoSlrRf Date: Wed, 20 May 2026 14:06:49 +0200 Subject: [PATCH 04/11] fix: expectNonOwnerReverts --- packages/smart-contracts/test/tron/helpers.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/smart-contracts/test/tron/helpers.js b/packages/smart-contracts/test/tron/helpers.js index 9efc5959e2..2ba73b6920 100644 --- a/packages/smart-contracts/test/tron/helpers.js +++ b/packages/smart-contracts/test/tron/helpers.js @@ -107,14 +107,11 @@ const assertBatchTokenBalancesZero = async (batch, tokens) => { */ const expectNonOwnerReverts = async (fn, getState) => { const before = await getState(); - let threw = false; try { await fn(); - } catch (_error) { - threw = true; - } + } catch (_error) {} await waitForConfirmation(2000); - assert(threw, 'expected non-owner call to revert'); + if (getState) { const after = await getState(); assert.equal(after, before, 'state should be unchanged after failed non-owner call'); From 92b7657438c30d76a0fe77320687b06577eb5348 Mon Sep 17 00:00:00 2001 From: LeoSlrRf Date: Wed, 20 May 2026 14:43:33 +0200 Subject: [PATCH 05/11] fix: remove return statement --- packages/smart-contracts/test/tron/helpers.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/smart-contracts/test/tron/helpers.js b/packages/smart-contracts/test/tron/helpers.js index 2ba73b6920..3fd05d4242 100644 --- a/packages/smart-contracts/test/tron/helpers.js +++ b/packages/smart-contracts/test/tron/helpers.js @@ -112,11 +112,8 @@ const expectNonOwnerReverts = async (fn, getState) => { } catch (_error) {} await waitForConfirmation(2000); - if (getState) { - const after = await getState(); - assert.equal(after, before, 'state should be unchanged after failed non-owner call'); - } - return { reverted: threw }; + const after = await getState(); + assert.equal(after, before, 'state should be unchanged after failed non-owner call'); }; /** From b91110487e047dd64677aaa6d341f7ac59930f37 Mon Sep 17 00:00:00 2001 From: LeoSlrRf Date: Thu, 21 May 2026 20:37:09 +0200 Subject: [PATCH 06/11] chore: remove contract ownership --- .../test/tron/ERC20BatchPayments.test.js | 21 +------------------ .../tron/contracts/ERC20BatchPayments.sol | 15 ++----------- 2 files changed, 3 insertions(+), 33 deletions(-) diff --git a/packages/smart-contracts/test/tron/ERC20BatchPayments.test.js b/packages/smart-contracts/test/tron/ERC20BatchPayments.test.js index 866a6d20a3..a57ab68f68 100644 --- a/packages/smart-contracts/test/tron/ERC20BatchPayments.test.js +++ b/packages/smart-contracts/test/tron/ERC20BatchPayments.test.js @@ -12,7 +12,6 @@ const { deployTokenWithSupply, expectRevertOrNoBalanceChange, assertBatchTokenBalancesZero, - expectNonOwnerReverts, deployBadTRC20, sumStrings, mulString, @@ -34,7 +33,7 @@ contract('ERC20BatchPayments Tron Test Suite', (accounts) => { before(async () => { const setup = await deployBaseSetup({ accounts, - batchDeployFn: (erc20FeeProxy, owner) => ERC20BatchPayments.new(erc20FeeProxy.address, owner), + batchDeployFn: (erc20FeeProxy) => ERC20BatchPayments.new(erc20FeeProxy.address), }); batch = setup.batch; [token1, token2, token3] = setup.tokens; @@ -750,22 +749,4 @@ contract('ERC20BatchPayments Tron Test Suite', (accounts) => { }); }); }); - - describe('Admin', () => { - describe('setPaymentErc20FeeProxy', () => { - it('should allow owner to update proxy addresses', async () => { - const ERC20FeeProxy = artifacts.require('ERC20FeeProxy'); - const newProxy = await ERC20FeeProxy.new(); - await batch.setPaymentErc20FeeProxy(newProxy.address, { from: payer }); - assert.equal(await batch.paymentErc20FeeProxy(), newProxy.address); - }); - - it('should revert when a non-owner tries to update proxy addresses', async () => { - await expectNonOwnerReverts( - () => batch.setPaymentErc20FeeProxy(payee1, { from: payee1 }), - async () => await batch.paymentErc20FeeProxy(), - ); - }); - }); - }); }); diff --git a/packages/smart-contracts/tron/contracts/ERC20BatchPayments.sol b/packages/smart-contracts/tron/contracts/ERC20BatchPayments.sol index 0d1b85fcba..68e788d9a8 100644 --- a/packages/smart-contracts/tron/contracts/ERC20BatchPayments.sol +++ b/packages/smart-contracts/tron/contracts/ERC20BatchPayments.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import '@openzeppelin/contracts/access/Ownable.sol'; import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import './interfaces/ERC20FeeProxy.sol'; import './lib/SafeERC20.sol'; @@ -14,7 +13,7 @@ import './lib/SafeERC20.sol'; * Make sure this contract has allowance to spend the payer's tokens. * Make sure the payer has enough tokens to pay the amounts and fees. */ -contract ERC20BatchPayments is Ownable { +contract ERC20BatchPayments { using SafeERC20 for IERC20; IERC20FeeProxy public paymentErc20FeeProxy; @@ -26,11 +25,9 @@ contract ERC20BatchPayments is Ownable { /** * @param _paymentErc20FeeProxy The address of the ERC20FeeProxy to use. - * @param _owner Owner of the contract. */ - constructor(address _paymentErc20FeeProxy, address _owner) { + constructor(address _paymentErc20FeeProxy) { paymentErc20FeeProxy = IERC20FeeProxy(_paymentErc20FeeProxy); - transferOwnership(_owner); } /** @@ -145,14 +142,6 @@ contract ERC20BatchPayments is Ownable { require(erc20.safeApprove(address(paymentErc20FeeProxy), max), 'approve() failed'); } - /** - * @notice Updates the ERC20FeeProxy address. - * @param _paymentErc20FeeProxy The address of the ERC20FeeProxy to use. - */ - function setPaymentErc20FeeProxy(address _paymentErc20FeeProxy) public onlyOwner { - paymentErc20FeeProxy = IERC20FeeProxy(_paymentErc20FeeProxy); - } - /** * @notice Pulls tokens from the payer to this contract and approves the proxy to spend them. * @param requestedToken The token to pay. From 9822c99e8432c5e2f55457678dfbe81ca9b87d5f Mon Sep 17 00:00:00 2001 From: LeoSlrRf Date: Tue, 26 May 2026 19:28:13 +0200 Subject: [PATCH 07/11] chore: remove BatchPayments contract --- .../test/tron/BatchPayments.test.js | 942 ------------------ .../tron/contracts/BatchPayments.sol | 1 - 2 files changed, 943 deletions(-) delete mode 100644 packages/smart-contracts/test/tron/BatchPayments.test.js delete mode 120000 packages/smart-contracts/tron/contracts/BatchPayments.sol diff --git a/packages/smart-contracts/test/tron/BatchPayments.test.js b/packages/smart-contracts/test/tron/BatchPayments.test.js deleted file mode 100644 index a024230ad8..0000000000 --- a/packages/smart-contracts/test/tron/BatchPayments.test.js +++ /dev/null @@ -1,942 +0,0 @@ -const BatchPayments = artifacts.require('BatchPayments'); -const { - REF_A, - REF_B, - REF_C, - waitForConfirmation, - balanceOf, - diff, - deployBaseSetup, - makeTokenApproval, - deployTokenWithSupply, - expectRevertOrNoBalanceChange, - assertBatchTokenBalancesZero, - expectNonOwnerReverts, - deployBadTRC20, - sumStrings, - mulString, - computeBatchFee, - getApprovalAmount, - trxBalance, - ONE_TRX_SUN, - TRON_ZERO_ADDRESS, -} = require('./helpers'); - -contract('BatchPayments Tron Test Suite', (accounts) => { - const payer = accounts[0]; - const payee1 = accounts[1] || 'TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE'; - const payee2 = accounts[2] || 'TG3XXyExBkPp9nzdajDZsozEu4BkaSJozs'; - const payee3 = accounts[3] || 'TFwt56qg984vEmk2UoDqUDeZhWEFSDaTmk'; - const feeAddress = accounts[4] || 'TNPGB28MjVCnEhTfpW51C2Ap3ZNnqGDXLB'; - - const BATCH_FEE_BPS = 10; - - let batch; - let token1; - let token2; - let token3; - - before(async () => { - const setup = await deployBaseSetup({ - accounts, - batchDeployFn: (erc20FeeProxy, owner, ethProxy) => - BatchPayments.new(erc20FeeProxy.address, ethProxy, owner), - batchFee: BATCH_FEE_BPS, - }); - batch = setup.batch; - [token1, token2, token3] = setup.tokens; - - console.log('\n=== BatchPayments (main) Test Setup ==='); - console.log('Batch:', batch.address); - console.log('Token1:', token1.address); - await waitForConfirmation(3000); - }); - - beforeEach(async () => { - await waitForConfirmation(2000); - }); - - describe('Happy Path Payment Scenarios', () => { - describe('batchERC20PaymentsWithReference', () => { - it('should pay 3 ERC20 payments', async () => { - const amount1 = '2000'; - const amount2 = '300'; - const amount3 = '400'; - const fee1 = '200'; - const fee2 = '20'; - const fee3 = '30'; - - const batchFee = computeBatchFee(sumStrings([amount1, amount2, amount3]), BATCH_FEE_BPS); - const totalPaymentAndFees = sumStrings([ - amount1, - amount2, - amount3, - fee1, - fee2, - fee3, - batchFee, - ]); - - await makeTokenApproval( - token1, - payer, - batch.address, - getApprovalAmount([amount1, amount2, amount3], [fee1, fee2, fee3], batchFee), - ); - - const payee1Before = await balanceOf(token1, payee1); - const payee2Before = await balanceOf(token1, payee2); - const payerBefore = await balanceOf(token1, payer); - const feeBefore = await balanceOf(token1, feeAddress); - - await batch.batchERC20PaymentsWithReference( - token1.address, - [payee1, payee2, payee2], - [amount1, amount2, amount3], - [REF_A, REF_B, REF_C], - [fee1, fee2, fee3], - feeAddress, - { from: payer }, - ); - await waitForConfirmation(3000); - - const payee1After = await balanceOf(token1, payee1); - const payee2After = await balanceOf(token1, payee2); - const payerAfter = await balanceOf(token1, payer); - const feeAfter = await balanceOf(token1, feeAddress); - - assert.equal(diff(payee1After, payee1Before).toString(), amount1); - assert.equal(diff(payee2After, payee2Before).toString(), sumStrings([amount2, amount3])); - assert.equal( - diff(feeAfter, feeBefore).toString(), - sumStrings([fee1, fee2, fee3, batchFee]), - ); - assert( - diff(payerBefore, payerAfter) >= BigInt(totalPaymentAndFees), - 'payer should pay amounts, fees, and batch fee', - ); - }); - - it('should pay 10 ERC20 payments', async () => { - const amount = '200'; - const feeAmount = '100'; - const nbTxs = 10; - - const batchFeeTotal = computeBatchFee(mulString(amount, nbTxs), BATCH_FEE_BPS); - - await makeTokenApproval( - token1, - payer, - batch.address, - getApprovalAmount(Array(nbTxs).fill(amount), Array(nbTxs).fill(feeAmount), batchFeeTotal), - ); - - const payee1Before = await balanceOf(token1, payee1); - const feeBefore = await balanceOf(token1, feeAddress); - - await batch.batchERC20PaymentsWithReference( - token1.address, - Array(nbTxs).fill(payee1), - Array(nbTxs).fill(amount), - Array(nbTxs).fill(REF_A), - Array(nbTxs).fill(feeAmount), - feeAddress, - { from: payer }, - ); - await waitForConfirmation(3000); - - assert.equal( - diff(await balanceOf(token1, payee1), payee1Before).toString(), - mulString(amount, nbTxs), - ); - assert.equal( - diff(await balanceOf(token1, feeAddress), feeBefore).toString(), - sumStrings([mulString(feeAmount, nbTxs), batchFeeTotal]), - ); - }); - - it('should leave no token balance on the batch contract after a successful payment', async () => { - const amount1 = '100'; - const fee1 = '10'; - - await makeTokenApproval( - token1, - payer, - batch.address, - getApprovalAmount([amount1], [fee1], computeBatchFee(amount1, BATCH_FEE_BPS)), - ); - - await batch.batchERC20PaymentsWithReference( - token1.address, - [payee1], - [amount1], - [REF_A], - [fee1], - feeAddress, - { from: payer }, - ); - await waitForConfirmation(3000); - - await assertBatchTokenBalancesZero(batch, [token1]); - }); - - it('should apply an updated batch fee on the next payment', async () => { - const newBatchFeeBps = 50; - await batch.setBatchFee(newBatchFeeBps, { from: payer }); - - const amount1 = '1000'; - const fee1 = '10'; - const batchFee = computeBatchFee(amount1, newBatchFeeBps); - - await makeTokenApproval( - token1, - payer, - batch.address, - getApprovalAmount([amount1], [fee1], batchFee), - ); - - const feeBefore = await balanceOf(token1, feeAddress); - - await batch.batchERC20PaymentsWithReference( - token1.address, - [payee1], - [amount1], - [REF_A], - [fee1], - feeAddress, - { from: payer }, - ); - await waitForConfirmation(3000); - - assert.equal( - diff(await balanceOf(token1, feeAddress), feeBefore).toString(), - sumStrings([fee1, batchFee]), - ); - - await batch.setBatchFee(BATCH_FEE_BPS, { from: payer }); - }); - - it('should pay ERC20 payments with no batch fee when batch fee is zero', async () => { - await batch.setBatchFee(0, { from: payer }); - - const amount1 = '500'; - const fee1 = '25'; - - await makeTokenApproval(token1, payer, batch.address, getApprovalAmount([amount1], [fee1])); - - const feeBefore = await balanceOf(token1, feeAddress); - - await batch.batchERC20PaymentsWithReference( - token1.address, - [payee1], - [amount1], - [REF_A], - [fee1], - feeAddress, - { from: payer }, - ); - await waitForConfirmation(3000); - - assert.equal(diff(await balanceOf(token1, feeAddress), feeBefore).toString(), fee1); - - await batch.setBatchFee(BATCH_FEE_BPS, { from: payer }); - }); - }); - - describe('batchERC20PaymentsMultiTokensWithReference', () => { - it('should pay 3 ERC20 payments in three different tokens', async () => { - const amount1 = '5000'; - const amount2 = '3000'; - const amount3 = '4000'; - const fee1 = '600'; - const fee2 = '200'; - const fee3 = '300'; - - const batchFee1 = computeBatchFee(amount1, BATCH_FEE_BPS); - const batchFee2 = computeBatchFee(amount2, BATCH_FEE_BPS); - const batchFee3 = computeBatchFee(amount3, BATCH_FEE_BPS); - - await makeTokenApproval( - token1, - payer, - batch.address, - getApprovalAmount([amount1], [fee1], batchFee1), - ); - await makeTokenApproval( - token2, - payer, - batch.address, - getApprovalAmount([amount2], [fee2], batchFee2), - ); - await makeTokenApproval( - token3, - payer, - batch.address, - getApprovalAmount([amount3], [fee3], batchFee3), - ); - - const payee1Before = await balanceOf(token1, payee1); - const payee2Token2Before = await balanceOf(token2, payee2); - const payee2Token3Before = await balanceOf(token3, payee2); - const feeToken1Before = await balanceOf(token1, feeAddress); - const feeToken2Before = await balanceOf(token2, feeAddress); - const feeToken3Before = await balanceOf(token3, feeAddress); - const payerToken1Before = await balanceOf(token1, payer); - const payerToken2Before = await balanceOf(token2, payer); - const payerToken3Before = await balanceOf(token3, payer); - - await batch.batchERC20PaymentsMultiTokensWithReference( - [token1.address, token2.address, token3.address], - [payee1, payee2, payee2], - [amount1, amount2, amount3], - [REF_A, REF_B, REF_C], - [fee1, fee2, fee3], - feeAddress, - { from: payer }, - ); - await waitForConfirmation(3000); - - assert.equal(diff(await balanceOf(token1, payee1), payee1Before).toString(), amount1); - assert.equal(diff(await balanceOf(token2, payee2), payee2Token2Before).toString(), amount2); - assert.equal(diff(await balanceOf(token3, payee2), payee2Token3Before).toString(), amount3); - assert.equal( - diff(await balanceOf(token1, feeAddress), feeToken1Before).toString(), - sumStrings([fee1, batchFee1]), - ); - assert.equal( - diff(await balanceOf(token2, feeAddress), feeToken2Before).toString(), - sumStrings([fee2, batchFee2]), - ); - assert.equal( - diff(await balanceOf(token3, feeAddress), feeToken3Before).toString(), - sumStrings([fee3, batchFee3]), - ); - - const total1 = sumStrings([amount1, fee1, batchFee1]); - const total2 = sumStrings([amount2, fee2, batchFee2]); - const total3 = sumStrings([amount3, fee3, batchFee3]); - assert( - diff(payerToken1Before, await balanceOf(token1, payer)) >= BigInt(total1), - 'payer should pay token1 amounts, fees, and batch fee', - ); - assert( - diff(payerToken2Before, await balanceOf(token2, payer)) >= BigInt(total2), - 'payer should pay token2 amounts, fees, and batch fee', - ); - assert( - diff(payerToken3Before, await balanceOf(token3, payer)) >= BigInt(total3), - 'payer should pay token3 amounts, fees, and batch fee', - ); - }); - - it('should pay 3 ERC20 payments in three different tokens with a zero amount payment', async () => { - const amount1 = '5000'; - const amount2 = '0'; - const amount3 = '4000'; - const fee1 = '600'; - const fee2 = '0'; - const fee3 = '300'; - - const batchFee1 = computeBatchFee(amount1, BATCH_FEE_BPS); - const batchFee3 = computeBatchFee(amount3, BATCH_FEE_BPS); - - await makeTokenApproval( - token1, - payer, - batch.address, - getApprovalAmount([amount1], [fee1], batchFee1), - ); - await makeTokenApproval(token2, payer, batch.address, getApprovalAmount([amount2], [fee2])); - await makeTokenApproval( - token3, - payer, - batch.address, - getApprovalAmount([amount3], [fee3], batchFee3), - ); - - const payee1Before = await balanceOf(token1, payee1); - const payee2Token2Before = await balanceOf(token2, payee2); - const payee2Token3Before = await balanceOf(token3, payee2); - - await batch.batchERC20PaymentsMultiTokensWithReference( - [token1.address, token2.address, token3.address], - [payee1, payee2, payee2], - [amount1, amount2, amount3], - [REF_A, REF_B, REF_C], - [fee1, fee2, fee3], - feeAddress, - { from: payer }, - ); - await waitForConfirmation(3000); - - assert.equal(diff(await balanceOf(token1, payee1), payee1Before).toString(), amount1); - assert.equal(diff(await balanceOf(token2, payee2), payee2Token2Before).toString(), amount2); - assert.equal(diff(await balanceOf(token3, payee2), payee2Token3Before).toString(), amount3); - }); - - it('should pay 4 ERC20 payments in two different tokens', async () => { - const amount1 = '200'; - const amount2 = '200'; - const amount3 = '200'; - const amount4 = '200'; - const fee1 = '10'; - const fee2 = '10'; - const fee3 = '10'; - const fee4 = '10'; - - const batchFee1 = computeBatchFee(sumStrings([amount1, amount2]), BATCH_FEE_BPS); - const batchFee2 = computeBatchFee(sumStrings([amount3, amount4]), BATCH_FEE_BPS); - - await makeTokenApproval( - token1, - payer, - batch.address, - getApprovalAmount([amount1, amount2], [fee1, fee2], batchFee1), - ); - await makeTokenApproval( - token2, - payer, - batch.address, - getApprovalAmount([amount3, amount4], [fee3, fee4], batchFee2), - ); - - const payee2Token1Before = await balanceOf(token1, payee2); - const payee2Token2Before = await balanceOf(token2, payee2); - - await batch.batchERC20PaymentsMultiTokensWithReference( - [token1.address, token1.address, token2.address, token2.address], - [payee2, payee2, payee2, payee2], - [amount1, amount2, amount3, amount4], - [REF_A, REF_A, REF_A, REF_A], - [fee1, fee2, fee3, fee4], - feeAddress, - { from: payer }, - ); - await waitForConfirmation(3000); - - assert.equal( - diff(await balanceOf(token1, payee2), payee2Token1Before).toString(), - sumStrings([amount1, amount2]), - ); - assert.equal( - diff(await balanceOf(token2, payee2), payee2Token2Before).toString(), - sumStrings([amount3, amount4]), - ); - }); - - it('should pay 10 ERC20 payments in two different tokens', async () => { - const amount = '20'; - const feeAmount = '10'; - const nbPaymentsPerToken = 5; - - const batchFee1 = computeBatchFee(mulString(amount, nbPaymentsPerToken), BATCH_FEE_BPS); - - await makeTokenApproval( - token1, - payer, - batch.address, - getApprovalAmount( - Array(nbPaymentsPerToken).fill(amount), - Array(nbPaymentsPerToken).fill(feeAmount), - batchFee1, - ), - ); - await makeTokenApproval( - token2, - payer, - batch.address, - getApprovalAmount( - Array(nbPaymentsPerToken).fill(amount), - Array(nbPaymentsPerToken).fill(feeAmount), - batchFee1, - ), - ); - - const payee1Token1Before = await balanceOf(token1, payee1); - const payee1Token2Before = await balanceOf(token2, payee1); - - await batch.batchERC20PaymentsMultiTokensWithReference( - [ - ...Array(nbPaymentsPerToken).fill(token1.address), - ...Array(nbPaymentsPerToken).fill(token2.address), - ], - Array(nbPaymentsPerToken * 2).fill(payee1), - Array(nbPaymentsPerToken * 2).fill(amount), - Array(nbPaymentsPerToken * 2).fill(REF_A), - Array(nbPaymentsPerToken * 2).fill(feeAmount), - feeAddress, - { from: payer }, - ); - await waitForConfirmation(3000); - - assert.equal( - diff(await balanceOf(token1, payee1), payee1Token1Before).toString(), - mulString(amount, nbPaymentsPerToken), - ); - assert.equal( - diff(await balanceOf(token2, payee1), payee1Token2Before).toString(), - mulString(amount, nbPaymentsPerToken), - ); - }); - - it('should leave no token balance on the batch contract after a successful payment', async () => { - const amount1 = '100'; - const amount2 = '200'; - const fee1 = '10'; - const fee2 = '20'; - - const batchFee1 = computeBatchFee(amount1, BATCH_FEE_BPS); - const batchFee2 = computeBatchFee(amount2, BATCH_FEE_BPS); - - await makeTokenApproval( - token1, - payer, - batch.address, - getApprovalAmount([amount1], [fee1], batchFee1), - ); - await makeTokenApproval( - token2, - payer, - batch.address, - getApprovalAmount([amount2], [fee2], batchFee2), - ); - - await batch.batchERC20PaymentsMultiTokensWithReference( - [token1.address, token2.address], - [payee1, payee2], - [amount1, amount2], - [REF_A, REF_B], - [fee1, fee2], - feeAddress, - { from: payer }, - ); - await waitForConfirmation(3000); - - await assertBatchTokenBalancesZero(batch, [token1, token2]); - }); - - it('should pay a multi-token ERC20 payment with BadTRC20', async () => { - const badToken = await deployBadTRC20(payer); - const paymentAmount = '100'; - const feeAmount = '10'; - const amount1 = '50'; - const fee1 = '5'; - - try { - await badToken.approve(batch.address, getApprovalAmount([paymentAmount], [feeAmount]), { - from: payer, - }); - await makeTokenApproval( - token1, - payer, - batch.address, - getApprovalAmount([amount1], [fee1], computeBatchFee(amount1, BATCH_FEE_BPS)), - ); - await waitForConfirmation(3000); - - const badPayeeBefore = await balanceOf(badToken, payee1); - const payee1Before = await balanceOf(token1, payee2); - - await batch.batchERC20PaymentsMultiTokensWithReference( - [badToken.address, token1.address], - [payee1, payee2], - [paymentAmount, amount1], - [REF_A, REF_B], - [feeAmount, fee1], - feeAddress, - { from: payer }, - ); - await waitForConfirmation(3000); - - const badPayeeAfter = await balanceOf(badToken, payee1); - const payee1After = await balanceOf(token1, payee2); - assert( - badPayeeAfter > badPayeeBefore || payee1After > payee1Before, - 'BadTRC20 multi-token: at least one payee balance should increase when batch succeeds', - ); - } catch (_error) { - console.log( - 'BadTRC20 multi-token batch payment rejected by Tron (acceptable for non-standard tokens)', - ); - } - }); - }); - }); - - describe('Error cases scenarios', () => { - describe('batchERC20PaymentsWithReference', () => { - it('should revert when the payer does not have enough funds to pay', async () => { - const amount1 = '5'; - const amount2 = '30'; - const amount3 = '400'; - const fee1 = '1'; - const fee2 = '2'; - const fee3 = '3'; - - const lowToken = await deployTokenWithSupply('100', payer); - await makeTokenApproval( - lowToken, - payer, - batch.address, - getApprovalAmount([amount1, amount2, amount3], [fee1, fee2, fee3]), - ); - - const payee3Before = await balanceOf(lowToken, payee3); - const { unchanged } = await expectRevertOrNoBalanceChange( - () => - batch.batchERC20PaymentsWithReference( - lowToken.address, - [payee1, payee2, payee3], - [amount1, amount2, amount3], - [REF_A, REF_B, REF_C], - [fee1, fee2, fee3], - feeAddress, - { from: payer }, - ), - async () => [await balanceOf(lowToken, payee3)], - ); - - assert(unchanged, 'should not transfer when funds insufficient'); - assert.equal((await balanceOf(lowToken, payee3)).toString(), payee3Before.toString()); - }); - - it('should revert when the payer does not have enough funds to pay the batch fee', async () => { - const amount1 = '100'; - const amount2 = '200'; - const fee1 = '1'; - const fee2 = '2'; - const paymentTotal = sumStrings([amount1, amount2, fee1, fee2]); - - const lowToken = await deployTokenWithSupply(paymentTotal, payer); - await makeTokenApproval(lowToken, payer, batch.address, paymentTotal); - - const payee1Before = await balanceOf(lowToken, payee1); - const { unchanged } = await expectRevertOrNoBalanceChange( - () => - batch.batchERC20PaymentsWithReference( - lowToken.address, - [payee1, payee2], - [amount1, amount2], - [REF_A, REF_B], - [fee1, fee2], - feeAddress, - { from: payer }, - ), - async () => [await balanceOf(lowToken, payee1)], - ); - - assert(unchanged, 'should not transfer when batch fee cannot be paid'); - assert.equal((await balanceOf(lowToken, payee1)).toString(), payee1Before.toString()); - }); - - it('should revert when the payer did not approve the batch contract to spend the tokens', async () => { - const amount1 = '20'; - const amount2 = '30'; - const amount3 = '40'; - const fee1 = '1'; - const fee2 = '2'; - const fee3 = '3'; - - await makeTokenApproval( - token1, - payer, - batch.address, - getApprovalAmount([amount1, amount2, amount3], [fee1, fee2, fee3]), - ); - await token1.approve(batch.address, '10', { from: payer }); - await waitForConfirmation(2000); - - const payee1Before = await balanceOf(token1, payee1); - const { unchanged } = await expectRevertOrNoBalanceChange( - () => - batch.batchERC20PaymentsWithReference( - token1.address, - [payee1, payee2, payee3], - [amount1, amount2, amount3], - [REF_A, REF_B, REF_C], - [fee1, fee2, fee3], - feeAddress, - { from: payer }, - ), - async () => [await balanceOf(token1, payee1)], - ); - - assert(unchanged, 'should not transfer without allowance'); - assert.equal((await balanceOf(token1, payee1)).toString(), payee1Before.toString()); - }); - - it('should revert when input arrays have different lengths', async () => { - const amount1 = '100'; - const fee1 = '1'; - const fee2 = '2'; - - await makeTokenApproval( - token1, - payer, - batch.address, - getApprovalAmount([amount1], [fee1, fee2]), - ); - - const payee1Before = await balanceOf(token1, payee1); - const payee2Before = await balanceOf(token1, payee2); - - const { unchanged } = await expectRevertOrNoBalanceChange( - () => - batch.batchERC20PaymentsWithReference( - token1.address, - [payee1, payee2], - [amount1], - [REF_A, REF_B], - [fee1, fee2], - feeAddress, - { from: payer }, - ), - async () => [await balanceOf(token1, payee1), await balanceOf(token1, payee2)], - ); - - assert(unchanged, 'should not transfer when array lengths mismatch'); - assert.equal((await balanceOf(token1, payee1)).toString(), payee1Before.toString()); - assert.equal((await balanceOf(token1, payee2)).toString(), payee2Before.toString()); - }); - }); - - describe('batchERC20PaymentsMultiTokensWithReference', () => { - it('should revert when the payer does not have enough funds to pay in at least one of the tokens', async () => { - const amount1 = '5'; - const amount2 = '30'; - const amount3 = '400'; - const fee1 = '1'; - const fee2 = '2'; - const fee3 = '3'; - - const lowToken = await deployTokenWithSupply('400', payer); - await makeTokenApproval( - lowToken, - payer, - batch.address, - getApprovalAmount([amount1, amount2, amount3], [fee1, fee2, fee3]), - ); - - const payee3Before = await balanceOf(lowToken, payee3); - const { unchanged } = await expectRevertOrNoBalanceChange( - () => - batch.batchERC20PaymentsMultiTokensWithReference( - [lowToken.address, lowToken.address, lowToken.address], - [payee1, payee2, payee3], - [amount1, amount2, amount3], - [REF_A, REF_B, REF_C], - [fee1, fee2, fee3], - feeAddress, - { from: payer }, - ), - async () => [await balanceOf(lowToken, payee3)], - ); - - assert(unchanged, 'multi-token batch should not transfer when funds insufficient'); - assert.equal((await balanceOf(lowToken, payee3)).toString(), payee3Before.toString()); - }); - - it('should revert when the payer does not have enough funds to pay the batch fee in at least one of the tokens', async () => { - const amount1 = '100'; - const amount2 = '200'; - const amount3 = '300'; - const fee1 = '1'; - const fee2 = '2'; - const fee3 = '3'; - - const lowToken = await deployTokenWithSupply('607', payer); - await makeTokenApproval( - lowToken, - payer, - batch.address, - getApprovalAmount([amount1, amount2, amount3], [fee1, fee2, fee3]), - ); - - const payee2Before = await balanceOf(lowToken, payee2); - const { unchanged } = await expectRevertOrNoBalanceChange( - () => - batch.batchERC20PaymentsMultiTokensWithReference( - [lowToken.address, lowToken.address, lowToken.address], - [payee1, payee2, payee2], - [amount1, amount2, amount3], - [REF_A, REF_B, REF_C], - [fee1, fee2, fee3], - feeAddress, - { from: payer }, - ), - async () => [await balanceOf(lowToken, payee2)], - ); - - assert(unchanged, 'multi-token batch should not transfer when batch fee cannot be paid'); - assert.equal((await balanceOf(lowToken, payee2)).toString(), payee2Before.toString()); - }); - - it('should revert when the payer did not approve the batch contract to spend the tokens in at least one of the tokens', async () => { - const amount1 = '100'; - const amount2 = '200'; - const amount3 = '300'; - const fee1 = '1'; - const fee2 = '2'; - const fee3 = '3'; - - await makeTokenApproval( - token1, - payer, - batch.address, - getApprovalAmount([amount1, amount2, amount3], [fee1, fee2, fee3]), - ); - await token1.approve(batch.address, '10', { from: payer }); - await waitForConfirmation(2000); - - const payee1Before = await balanceOf(token1, payee1); - const { unchanged } = await expectRevertOrNoBalanceChange( - () => - batch.batchERC20PaymentsMultiTokensWithReference( - [token1.address, token1.address, token1.address], - [payee1, payee2, payee3], - [amount1, amount2, amount3], - [REF_A, REF_B, REF_C], - [fee1, fee2, fee3], - feeAddress, - { from: payer }, - ), - async () => [await balanceOf(token1, payee1)], - ); - - assert(unchanged, 'multi-token batch should not transfer without allowance'); - assert.equal((await balanceOf(token1, payee1)).toString(), payee1Before.toString()); - }); - - it('should revert when the payer did not approve the batch contract for one of the tokens', async () => { - const amount1 = '100'; - const amount2 = '200'; - const fee1 = '1'; - const fee2 = '2'; - - await makeTokenApproval(token1, payer, batch.address, getApprovalAmount([amount1], [fee1])); - - const payee2Token2Before = await balanceOf(token2, payee2); - const { unchanged } = await expectRevertOrNoBalanceChange( - () => - batch.batchERC20PaymentsMultiTokensWithReference( - [token1.address, token2.address], - [payee1, payee2], - [amount1, amount2], - [REF_A, REF_B], - [fee1, fee2], - feeAddress, - { from: payer }, - ), - async () => [await balanceOf(token2, payee2)], - ); - - assert(unchanged, 'should not transfer when one token lacks approval'); - assert.equal((await balanceOf(token2, payee2)).toString(), payee2Token2Before.toString()); - }); - - it('should revert when input arrays have different lengths', async () => { - const amount1 = '100'; - const fee1 = '1'; - const fee2 = '2'; - - await makeTokenApproval(token1, payer, batch.address, getApprovalAmount([amount1], [fee1])); - await makeTokenApproval(token2, payer, batch.address, getApprovalAmount([amount1], [fee2])); - - const payee1Before = await balanceOf(token1, payee1); - const payee2Before = await balanceOf(token2, payee2); - - const { unchanged } = await expectRevertOrNoBalanceChange( - () => - batch.batchERC20PaymentsMultiTokensWithReference( - [token1.address, token2.address], - [payee1, payee2], - [amount1], - [REF_A, REF_B], - [fee1, fee2], - feeAddress, - { from: payer }, - ), - async () => [await balanceOf(token1, payee1), await balanceOf(token2, payee2)], - ); - - assert(unchanged, 'should not transfer when array lengths mismatch'); - assert.equal((await balanceOf(token1, payee1)).toString(), payee1Before.toString()); - assert.equal((await balanceOf(token2, payee2)).toString(), payee2Before.toString()); - }); - }); - - describe('batchEthPaymentsWithReference', () => { - it('should revert when calling batchEthPaymentsWithReference when EthFeeProxy is not set', async () => { - assert.equal(await batch.paymentEthFeeProxy(), TRON_ZERO_ADDRESS); - - const paymentAmount = String(10 * ONE_TRX_SUN); - const feeAmount = '0'; - const payeeBefore = await trxBalance(payee1); - const payerBefore = await trxBalance(payer); - - try { - await batch.batchEthPaymentsWithReference( - [payee1], - [paymentAmount], - [REF_A], - [feeAmount], - feeAddress, - { from: payer, callValue: Number(paymentAmount) }, - ); - } catch (_error) {} - await waitForConfirmation(2000); - - assert.equal((await trxBalance(payee1)).toString(), payeeBefore.toString()); - - const payerSpent = payerBefore - (await trxBalance(payer)); - assert( - payerSpent < BigInt(paymentAmount), - `payer should only spend tx fees, not ${paymentAmount} sun (spent ${payerSpent})`, - ); - }); - }); - }); - - describe('Admin', () => { - describe('setPaymentErc20FeeProxy', () => { - it('should allow owner to update proxy addresses', async () => { - const ERC20FeeProxy = artifacts.require('ERC20FeeProxy'); - const newProxy = await ERC20FeeProxy.new(); - await batch.setPaymentErc20FeeProxy(newProxy.address, { from: payer }); - assert.equal(await batch.paymentErc20FeeProxy(), newProxy.address); - }); - - it('should revert when a non-owner tries to update proxy addresses', async () => { - await expectNonOwnerReverts( - () => batch.setPaymentErc20FeeProxy(payee1, { from: payee1 }), - async () => await batch.paymentErc20FeeProxy(), - ); - }); - }); - - describe('setBatchFee', () => { - it('should allow owner to update the batch fee', async () => { - const newBatchFee = 50; - await batch.setBatchFee(newBatchFee, { from: payer }); - assert.equal((await batch.batchFee()).toString(), String(newBatchFee)); - await batch.setBatchFee(BATCH_FEE_BPS, { from: payer }); - }); - - it('should revert when a non-owner tries to set the batch fee', async () => { - await expectNonOwnerReverts( - () => batch.setBatchFee(99, { from: payee1 }), - async () => (await batch.batchFee()).toString(), - ); - }); - }); - - describe('setPaymentEthFeeProxy', () => { - it('should revert when a non-owner tries to set the EthFeeProxy address', async () => { - await expectNonOwnerReverts( - () => batch.setPaymentEthFeeProxy(TRON_ZERO_ADDRESS, { from: payee1 }), - async () => await batch.paymentEthFeeProxy(), - ); - }); - }); - }); -}); diff --git a/packages/smart-contracts/tron/contracts/BatchPayments.sol b/packages/smart-contracts/tron/contracts/BatchPayments.sol deleted file mode 120000 index 5f16a2c12b..0000000000 --- a/packages/smart-contracts/tron/contracts/BatchPayments.sol +++ /dev/null @@ -1 +0,0 @@ -../../src/contracts/BatchPayments.sol \ No newline at end of file From bc5e65e20d4c4472badd13f174809706437456a8 Mon Sep 17 00:00:00 2001 From: LeoSlrRf Date: Tue, 26 May 2026 19:28:59 +0200 Subject: [PATCH 08/11] feat: extend tron deployment script to support ERC20BatchPayments --- .../scripts/tron/deploy-mainnet.js | 122 ++++++++++++++---- .../scripts/tron/deploy-nile.js | 12 ++ 2 files changed, 106 insertions(+), 28 deletions(-) diff --git a/packages/smart-contracts/scripts/tron/deploy-mainnet.js b/packages/smart-contracts/scripts/tron/deploy-mainnet.js index 873cca51c2..d3f9ec34dd 100644 --- a/packages/smart-contracts/scripts/tron/deploy-mainnet.js +++ b/packages/smart-contracts/scripts/tron/deploy-mainnet.js @@ -2,7 +2,7 @@ /** * Tron Mainnet Deployment Script * - * This script deploys the ERC20FeeProxy to Tron mainnet. + * This script deploys the ERC20FeeProxy and ERC20BatchPayments to Tron mainnet. * * ⚠️ WARNING: This deploys to MAINNET with real TRX! * @@ -28,6 +28,18 @@ const PRIVATE_KEY = process.env.TRON_PRIVATE_KEY; // Safety check const CONFIRM_MAINNET = process.env.CONFIRM_MAINNET_DEPLOY === 'true'; +const MAINNET_DEPLOYMENT_PATH = path.join(__dirname, '../../deployments/tron/mainnet.json'); + +/** + * Contracts to deploy + * + * Comment out the contracts you don't want to deploy. + */ +const CONTRACTS_TO_DEPLOY = [ + //'ERC20FeeProxy', + 'ERC20BatchPayments', +]; + if (!PRIVATE_KEY) { console.error('Error: TRON_PRIVATE_KEY environment variable is required'); process.exit(1); @@ -49,6 +61,13 @@ async function loadArtifact(contractName) { return JSON.parse(fs.readFileSync(artifactPath, 'utf8')); } +function loadExistingMainnetDeployment() { + if (!fs.existsSync(MAINNET_DEPLOYMENT_PATH)) { + return null; + } + return JSON.parse(fs.readFileSync(MAINNET_DEPLOYMENT_PATH, 'utf8')); +} + async function confirmDeployment() { if (CONFIRM_MAINNET) { return true; @@ -92,6 +111,24 @@ async function deployContract(contractName, constructorArgs = []) { }; } +async function deployContractWrapper({ + contractName, + deployments, + blockNumbers, + constructorArgs = [], +}) { + const contract = await deployContract(contractName, constructorArgs); + deployments[contractName] = { + address: contract.address, + hexAddress: contract.hexAddress, + }; + + // Get block number + const block = await tronWeb.trx.getCurrentBlock(); + const blockNumber = block.block_header.raw_data.number; + blockNumbers[contractName] = blockNumber; +} + async function main() { console.log('╔══════════════════════════════════════════════════════════╗'); console.log('║ TRON MAINNET DEPLOYMENT ║'); @@ -123,50 +160,80 @@ async function main() { console.log('\n🚀 Starting mainnet deployment...\n'); const deployments = {}; + const blockNumbers = {}; const startTime = Date.now(); try { - // Deploy ERC20FeeProxy only (no test tokens on mainnet) - const erc20FeeProxy = await deployContract('ERC20FeeProxy'); - deployments.ERC20FeeProxy = { - address: erc20FeeProxy.address, - hexAddress: erc20FeeProxy.hexAddress, - }; + const existingDeployment = loadExistingMainnetDeployment(); + + // Deploy ERC20FeeProxy + if (CONTRACTS_TO_DEPLOY.includes('ERC20FeeProxy')) { + await deployContractWrapper({ contractName: 'ERC20FeeProxy', deployments, blockNumbers }); + await new Promise((resolve) => setTimeout(resolve, 5000)); + } + + // Deploy ERC20BatchPayments + if (CONTRACTS_TO_DEPLOY.includes('ERC20BatchPayments')) { + const erc20FeeProxyAddress = deployments.ERC20FeeProxy + ? deployments.ERC20FeeProxy.address + : existingDeployment.contracts.ERC20FeeProxy.address; - // Get block number - const block = await tronWeb.trx.getCurrentBlock(); - const blockNumber = block.block_header.raw_data.number; + if (!erc20FeeProxyAddress) { + console.error( + 'ERC20FeeProxy address not found in deployments/tron/mainnet.json; cannot deploy ERC20BatchPayments', + ); + process.exit(1); + } + + console.log('Using ERC20FeeProxy at:', erc20FeeProxyAddress); + await deployContractWrapper({ + contractName: 'ERC20BatchPayments', + deployments, + blockNumbers, + constructorArgs: [erc20FeeProxyAddress], + }); + } // Print summary console.log('\n╔══════════════════════════════════════════════════════════╗'); console.log('║ MAINNET DEPLOYMENT SUMMARY ║'); console.log('╚══════════════════════════════════════════════════════════╝\n'); - console.log('ERC20FeeProxy:'); - console.log(` Address: ${deployments.ERC20FeeProxy.address}`); - console.log(` Block: ${blockNumber}`); - console.log( - ` Tronscan: https://tronscan.org/#/contract/${deployments.ERC20FeeProxy.address}`, - ); + for (const contractName of Object.keys(deployments)) { + console.log(`${contractName}:`); + console.log(` Address: ${deployments[contractName].address}`); + console.log(` Block: ${blockNumbers[contractName]}`); + console.log( + ` Tronscan: https://tronscan.org/#/contract/${deployments[contractName].address}`, + ); + } + + const newContracts = Object.entries(deployments).reduce((acc, [contractName, contract]) => { + acc[contractName] = { + ...contract, + creationBlockNumber: blockNumbers[contractName], + }; + return acc; + }, {}); + + const contracts = { + ...(existingDeployment.contracts || {}), + ...newContracts, + }; - // Save deployment info + // Save deployment info (merge with existing mainnet.json) const deploymentInfo = { network: 'mainnet', chainId: '1', timestamp: new Date().toISOString(), deployer: deployerAddress, deploymentDuration: `${(Date.now() - startTime) / 1000}s`, - contracts: { - ERC20FeeProxy: { - ...deployments.ERC20FeeProxy, - creationBlockNumber: blockNumber, - }, - }, + contracts, }; - const outputPath = path.join(__dirname, '../../deployments/tron/mainnet.json'); - fs.writeFileSync(outputPath, JSON.stringify(deploymentInfo, null, 2)); - console.log(`\nDeployment info saved to: ${outputPath}`); + fs.mkdirSync(path.dirname(MAINNET_DEPLOYMENT_PATH), { recursive: true }); + fs.writeFileSync(MAINNET_DEPLOYMENT_PATH, JSON.stringify(deploymentInfo, null, 2)); + console.log(`\nDeployment info saved to: ${MAINNET_DEPLOYMENT_PATH}`); // Next steps console.log('\n╔══════════════════════════════════════════════════════════╗'); @@ -174,8 +241,7 @@ async function main() { console.log('╚══════════════════════════════════════════════════════════╝\n'); console.log('1. Verify contract on Tronscan'); console.log('2. Run verification script: yarn tron:verify:mainnet'); - console.log('3. Update artifact registry in:'); - console.log(' packages/smart-contracts/src/lib/artifacts/ERC20FeeProxy/index.ts'); + console.log('3. Update artifact registry with new deployment addresses'); console.log('4. Test with a real TRC20 token payment'); } catch (error) { console.error('\n❌ Deployment failed:', error.message); diff --git a/packages/smart-contracts/scripts/tron/deploy-nile.js b/packages/smart-contracts/scripts/tron/deploy-nile.js index 9e5d21d8b8..5e83afeec2 100644 --- a/packages/smart-contracts/scripts/tron/deploy-nile.js +++ b/packages/smart-contracts/scripts/tron/deploy-nile.js @@ -91,6 +91,15 @@ async function main() { hexAddress: erc20FeeProxy.address, }; + // 2. Deploy ERC20BatchPayments + const erc20BatchPayments = await deployContract('ERC20BatchPayments', [ + deployments.ERC20FeeProxy.address, + ]); + deployments.ERC20BatchPayments = { + address: tronWeb.address.fromHex(erc20BatchPayments.address), + hexAddress: erc20BatchPayments.address, + }; + // 2. Deploy TestTRC20 for testing const testToken = await deployContract('TestTRC20', [ '1000000000000000000000000000', // 1 billion tokens @@ -141,6 +150,9 @@ async function main() { console.log('╚══════════════════════════════════════════════════════════╝\n'); console.log('1. Verify contracts on Nile Tronscan:'); console.log(' https://nile.tronscan.org/#/contract/' + deployments.ERC20FeeProxy.address); + console.log( + ' https://nile.tronscan.org/#/contract/' + deployments.ERC20BatchPayments.address, + ); console.log('\n2. Run tests against deployed contracts:'); console.log(' TRON_PRIVATE_KEY=... yarn tron:test:nile'); console.log('\n3. Update artifact registry with deployment addresses'); From 0ad5f2453a632d77ec73faa6ca9062ef55d2cc08 Mon Sep 17 00:00:00 2001 From: LeoSlrRf Date: Tue, 26 May 2026 19:29:25 +0200 Subject: [PATCH 09/11] add deployment information for ERC20BatchPayments --- .../deployments/tron/mainnet.json | 12 +- .../deployments/tron/nile.json | 5 + .../artifacts/ERC20BatchPayments/0.1.0.json | 117 ++++++++++++++++++ .../lib/artifacts/ERC20BatchPayments/index.ts | 24 ++++ 4 files changed, 155 insertions(+), 3 deletions(-) create mode 100644 packages/smart-contracts/src/lib/artifacts/ERC20BatchPayments/0.1.0.json create mode 100644 packages/smart-contracts/src/lib/artifacts/ERC20BatchPayments/index.ts diff --git a/packages/smart-contracts/deployments/tron/mainnet.json b/packages/smart-contracts/deployments/tron/mainnet.json index f1755f5186..7d7fdf8ff3 100644 --- a/packages/smart-contracts/deployments/tron/mainnet.json +++ b/packages/smart-contracts/deployments/tron/mainnet.json @@ -1,14 +1,20 @@ { "network": "mainnet", "chainId": "1", - "timestamp": "2024-01-01T00:00:00.000Z", - "deployer": "TO_BE_FILLED_ON_DEPLOYMENT", - "note": "Existing deployment from handover document", + "timestamp": "2026-05-26T17:02:20.157Z", + "deployer": "TR7EydtGnsxriSieLfEuspTAqhQRmoscWC", + "deploymentDuration": "3.244s", + "note": "Deployment of ERC20BatchPayments", "contracts": { "ERC20FeeProxy": { "address": "TCUDPYnS9dH3WvFEaE7wN7vnDa51J4R4fd", "hexAddress": "411b6ca35d39842cf8fbe49000653a1505412da659", "creationBlockNumber": 79216121 + }, + "ERC20BatchPayments": { + "address": "TUdcGd29QpV65MkbqgBLWJKbTG3UL7PuQB", + "hexAddress": "41ccb57e7bbb729b1c8c94294a61b658a6f2304281", + "creationBlockNumber": 83048743 } } } diff --git a/packages/smart-contracts/deployments/tron/nile.json b/packages/smart-contracts/deployments/tron/nile.json index 77257b8bc6..699e324f4e 100644 --- a/packages/smart-contracts/deployments/tron/nile.json +++ b/packages/smart-contracts/deployments/tron/nile.json @@ -9,6 +9,11 @@ "address": "THK5rNmrvCujhmrXa5DB1dASepwXTr9cJs", "hexAddress": "41508b3b4059c40bb3aac5da5ac006ccdd9c4dc957", "creationBlockNumber": 63208782 + }, + "ERC20BatchPayments": { + "address": "TDnU5eY8Et3QdZRWMSTvoXQnxQeMxF7CE4", + "hexAddress": "4129d883d52bf19f97ef0a0c2edb99a679b0d5e12e", + "creationBlockNumber": 67775288 } } } diff --git a/packages/smart-contracts/src/lib/artifacts/ERC20BatchPayments/0.1.0.json b/packages/smart-contracts/src/lib/artifacts/ERC20BatchPayments/0.1.0.json new file mode 100644 index 0000000000..62f2440ff3 --- /dev/null +++ b/packages/smart-contracts/src/lib/artifacts/ERC20BatchPayments/0.1.0.json @@ -0,0 +1,117 @@ +{ + "abi": [ + { + "inputs": [ + { + "internalType": "address", + "name": "_paymentErc20FeeProxy", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_erc20Address", + "type": "address" + } + ], + "name": "approvePaymentProxyToSpend", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "_tokenAddresses", + "type": "address[]" + }, + { + "internalType": "address[]", + "name": "_recipients", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "_amounts", + "type": "uint256[]" + }, + { + "internalType": "bytes[]", + "name": "_paymentReferences", + "type": "bytes[]" + }, + { + "internalType": "uint256[]", + "name": "_feeAmounts", + "type": "uint256[]" + }, + { + "internalType": "address", + "name": "_feeAddress", + "type": "address" + } + ], + "name": "batchERC20PaymentsMultiTokensWithReference", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_tokenAddress", + "type": "address" + }, + { + "internalType": "address[]", + "name": "_recipients", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "_amounts", + "type": "uint256[]" + }, + { + "internalType": "bytes[]", + "name": "_paymentReferences", + "type": "bytes[]" + }, + { + "internalType": "uint256[]", + "name": "_feeAmounts", + "type": "uint256[]" + }, + { + "internalType": "address", + "name": "_feeAddress", + "type": "address" + } + ], + "name": "batchERC20PaymentsWithReference", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "paymentErc20FeeProxy", + "outputs": [ + { + "internalType": "contract IERC20FeeProxy", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + } + ] +} diff --git a/packages/smart-contracts/src/lib/artifacts/ERC20BatchPayments/index.ts b/packages/smart-contracts/src/lib/artifacts/ERC20BatchPayments/index.ts new file mode 100644 index 0000000000..2826468f7a --- /dev/null +++ b/packages/smart-contracts/src/lib/artifacts/ERC20BatchPayments/index.ts @@ -0,0 +1,24 @@ +import { ContractArtifact } from '../../ContractArtifact'; + +import { abi as ABI_0_1_0 } from './0.1.0.json'; +// @ts-ignore Cannot find module +import type { ERC20BatchPayments } from '../../../types/tron'; + +export const erc20BatchPaymentsArtifact = new ContractArtifact( + { + tron: { + abi: ABI_0_1_0, + deployment: { + nile: { + address: 'THK5rNmrvCujhmrXa5DB1dASepwXTr9cJs', + creationBlockNumber: 63208782, + }, + tron: { + address: 'TUdcGd29QpV65MkbqgBLWJKbTG3UL7PuQB', + creationBlockNumber: 83048743, + }, + }, + }, + }, + '0.1.0', +); From 663ef2f6200529ac2fbba7cf576391c52a9a2d49 Mon Sep 17 00:00:00 2001 From: LeoSlrRf Date: Tue, 26 May 2026 20:05:38 +0200 Subject: [PATCH 10/11] update nile ERC20BatchPAyments deployment info --- .../src/lib/artifacts/ERC20BatchPayments/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/smart-contracts/src/lib/artifacts/ERC20BatchPayments/index.ts b/packages/smart-contracts/src/lib/artifacts/ERC20BatchPayments/index.ts index 2826468f7a..9e0c603c73 100644 --- a/packages/smart-contracts/src/lib/artifacts/ERC20BatchPayments/index.ts +++ b/packages/smart-contracts/src/lib/artifacts/ERC20BatchPayments/index.ts @@ -10,8 +10,8 @@ export const erc20BatchPaymentsArtifact = new ContractArtifact Date: Wed, 27 May 2026 11:30:20 +0200 Subject: [PATCH 11/11] contract update based on comments --- .../deployments/tron/mainnet.json | 12 +++++----- .../deployments/tron/nile.json | 6 ++--- .../lib/artifacts/ERC20BatchPayments/index.ts | 8 +++---- .../test/tron/ERC20BatchPayments.test.js | 22 +++++++++++++++++++ .../tron/contracts/ERC20BatchPayments.sol | 5 +++-- 5 files changed, 38 insertions(+), 15 deletions(-) diff --git a/packages/smart-contracts/deployments/tron/mainnet.json b/packages/smart-contracts/deployments/tron/mainnet.json index 7d7fdf8ff3..ae0df3ed79 100644 --- a/packages/smart-contracts/deployments/tron/mainnet.json +++ b/packages/smart-contracts/deployments/tron/mainnet.json @@ -1,10 +1,10 @@ { "network": "mainnet", "chainId": "1", - "timestamp": "2026-05-26T17:02:20.157Z", + "timestamp": "2026-05-27T09:23:48.624Z", "deployer": "TR7EydtGnsxriSieLfEuspTAqhQRmoscWC", - "deploymentDuration": "3.244s", - "note": "Deployment of ERC20BatchPayments", + "deploymentDuration": "3.227s", + "note": "Deployment of ERC20BatchPayments to Mainnet", "contracts": { "ERC20FeeProxy": { "address": "TCUDPYnS9dH3WvFEaE7wN7vnDa51J4R4fd", @@ -12,9 +12,9 @@ "creationBlockNumber": 79216121 }, "ERC20BatchPayments": { - "address": "TUdcGd29QpV65MkbqgBLWJKbTG3UL7PuQB", - "hexAddress": "41ccb57e7bbb729b1c8c94294a61b658a6f2304281", - "creationBlockNumber": 83048743 + "address": "THm8vX6GNfRFZ15mRqdgvj56wjB6575S7C", + "hexAddress": "4155789c40d8ba55166296217cc244ca2dd3499f89", + "creationBlockNumber": 83068367 } } } diff --git a/packages/smart-contracts/deployments/tron/nile.json b/packages/smart-contracts/deployments/tron/nile.json index 699e324f4e..818ebdd22a 100644 --- a/packages/smart-contracts/deployments/tron/nile.json +++ b/packages/smart-contracts/deployments/tron/nile.json @@ -11,9 +11,9 @@ "creationBlockNumber": 63208782 }, "ERC20BatchPayments": { - "address": "TDnU5eY8Et3QdZRWMSTvoXQnxQeMxF7CE4", - "hexAddress": "4129d883d52bf19f97ef0a0c2edb99a679b0d5e12e", - "creationBlockNumber": 67775288 + "address": "TC6nD547PRDVWuX8hBMREU7vVvSZNCAZot", + "hexAddress": "41175ee218bb15fc25224ab5937b0844f4f70a2b97", + "creationBlockNumber": 67794373 } } } diff --git a/packages/smart-contracts/src/lib/artifacts/ERC20BatchPayments/index.ts b/packages/smart-contracts/src/lib/artifacts/ERC20BatchPayments/index.ts index 9e0c603c73..534029b8c6 100644 --- a/packages/smart-contracts/src/lib/artifacts/ERC20BatchPayments/index.ts +++ b/packages/smart-contracts/src/lib/artifacts/ERC20BatchPayments/index.ts @@ -10,12 +10,12 @@ export const erc20BatchPaymentsArtifact = new ContractArtifact { }); }); }); + +contract('ERC20BatchPayments constructor', () => { + const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; + + it('should revert when paymentErc20FeeProxy is the zero address', async () => { + let reverted = false; + let errorMessage = ''; + + try { + await ERC20BatchPayments.new(ZERO_ADDRESS); + } catch (error) { + reverted = true; + errorMessage = error.message || String(error); + } + + assert(reverted, 'deployment should revert when paymentErc20FeeProxy is address(0)'); + assert( + errorMessage.includes('paymentErc20FeeProxy cannot be 0x'), + `expected zero-address revert, got: ${errorMessage}`, + ); + }); +}); diff --git a/packages/smart-contracts/tron/contracts/ERC20BatchPayments.sol b/packages/smart-contracts/tron/contracts/ERC20BatchPayments.sol index 68e788d9a8..4a190b0d60 100644 --- a/packages/smart-contracts/tron/contracts/ERC20BatchPayments.sol +++ b/packages/smart-contracts/tron/contracts/ERC20BatchPayments.sol @@ -16,7 +16,7 @@ import './lib/SafeERC20.sol'; contract ERC20BatchPayments { using SafeERC20 for IERC20; - IERC20FeeProxy public paymentErc20FeeProxy; + IERC20FeeProxy public immutable paymentErc20FeeProxy; struct Token { address tokenAddress; @@ -27,6 +27,7 @@ contract ERC20BatchPayments { * @param _paymentErc20FeeProxy The address of the ERC20FeeProxy to use. */ constructor(address _paymentErc20FeeProxy) { + require(_paymentErc20FeeProxy != address(0), 'ERC20BatchPayments: paymentErc20FeeProxy cannot be 0x'); paymentErc20FeeProxy = IERC20FeeProxy(_paymentErc20FeeProxy); } @@ -136,7 +137,7 @@ contract ERC20BatchPayments { * @notice Authorizes the proxy to spend a request currency (ERC20). * @param _erc20Address Address of an ERC20 used as the request currency. */ - function approvePaymentProxyToSpend(address _erc20Address) public { + function approvePaymentProxyToSpend(address _erc20Address) internal { IERC20 erc20 = IERC20(_erc20Address); uint256 max = type(uint256).max; require(erc20.safeApprove(address(paymentErc20FeeProxy), max), 'approve() failed');