diff --git a/contracts/src/Chainvoice.sol b/contracts/src/Chainvoice.sol index d2cd5219..7e85f878 100644 --- a/contracts/src/Chainvoice.sol +++ b/contracts/src/Chainvoice.sol @@ -312,7 +312,9 @@ contract Chainvoice { uint256 totalAmounts = 0; uint256 totalNativeFee = fee * n; - // Validate and sum + // Validate and mark paid in the same pass so a duplicate id in the + // batch hits AlreadySettled() on its second occurrence, before any + // funds move. for (uint256 i = 0; i < n; i++) { uint256 id = invoiceIds[i]; if (id >= invoices.length) revert InvalidInvoiceId(); @@ -324,11 +326,7 @@ contract Chainvoice { if (inv.tokenAddress != token) revert MixedTokenBatch(); totalAmounts += inv.amountDue; - } - - // Effects: mark all paid & bump fee accumulator BEFORE interactions - for (uint256 i = 0; i < n; i++) { - invoices[invoiceIds[i]].isPaid = true; + inv.isPaid = true; } accumulatedFees += totalNativeFee; // Interactions diff --git a/contracts/test/Chainvoice.t.sol b/contracts/test/Chainvoice.t.sol index 0d45976c..8b2733ae 100644 --- a/contracts/test/Chainvoice.t.sol +++ b/contracts/test/Chainvoice.t.sol @@ -196,6 +196,23 @@ contract ChainvoiceTest is Test { assertEq(alice.balance, aliceStart + totalPrincipal); } + function testPayInvoicesBatch_RevertOnDuplicateId() public { + vm.prank(alice); + chainvoice.createInvoice(bob, 1 ether, address(0), keccak256("dup1")); + + uint256 fee = chainvoice.fee(); + uint256 totalFee = fee * 2; + uint256 totalPrincipal = 2 ether; + + uint256[] memory ids = new uint256[](2); + ids[0] = 0; + ids[1] = 0; + + vm.prank(bob); + vm.expectRevert(Chainvoice.AlreadySettled.selector); + chainvoice.payInvoicesBatch{value: totalPrincipal + totalFee}(ids); + } + /* ------------------------------------------------------------ */ /* FUZZ TESTING */ /* ------------------------------------------------------------ */