The owner-only emergencyCancelPayment(venue, promoter) is intended to claw back a promoter’s outstanding credits to the venue. If the promoter credits are transferable, the promoter can front‑run the owner’s cancel by transferring credits to another address, making the cancel call a silent no‑op (burn/mint of 0) without reverting.
Vulnerability Details
Flow:
1
Step: Alice mints venue credits
Alice mints 100 venue credits.
2
Step: Bob mints promoter credits
Bob mints 50 promoter credits; and mints another 2 promoter credits with a proxy account. Backend signs a 2‑credit payout for proxy account.
3
Step: Owner triggers emergency cancel
Owner suspects Bob1 and calls emergencyCancelPayment(venue, Bob1).
4
Step: Promoter front‑runs
Bob front‑runs by transferring 50 credits from Bob → Bob's proxy account.
5
Step: Cancel becomes a no‑op
When the owner tx executes, promoterBalance(Bob) == 0, so emergencyCancelPayment burns 0 and mints 0; it does not revert or restore any credits. Combined with signature replay, Bob's proxy account (now holding 52 credits) replays the same signed payout in a loop to withdraw all 52 credits from escrow.
Root Cause:
emergencyCancelPayment reads the live promoter balance and unconditionally burns that amount, lacking a require(promoterBalance > 0) or expectedAmount parameter.
distributePromoterPayments has no nonce/expiry, allowing the same backend signature to be replayed multiple times.
Final Result — Bob's proxy account, now holding the transferred credits, can replay the signed payout multiple times to drain the escrow.
Impact Details
Owner’s cancellation can be bypassed by transferring credits to another address prior to cancellation.
With signature replayability, an attacker can drain escrow by repeatedly redeeming the same signed payout (example: 52 USDC in the PoC).
Severity (author): High (loss of funds and failure of admin control).
Add nonce/expiry to the backend-signed promoter payout messages so each signature can only be used once or before a deadline.
2
Hardening emergency cancellation against front‑running
Ensure emergencyCancelPayment cannot be trivially bypassed by transfers:
Consider requiring an expected amount parameter or verifying promoter balance at signature generation time.
Alternatively, add time-based locks on promoter withdrawals after a cancel is initiated, or check that the promoter still owns expected credits before burning/minting.
Proof of Concept
PoC test: emergencyCancelPayment front-run + signature replay drains escrow (add to belong-check-in-bsc-fork.test.ts)PoC test run output
$ npx hardhat test test/v2/platform/belong-check-in-bsc-fork.test.ts --grep "signature replay drains escrow"
secp256k1 unavailable, reverting to browser version
BelongCheckIn BSC PancakeSwap
Warning: Potentially unsafe deployment of contracts/v2/platform/Factory.sol:Factory
You are using the `unsafeAllow.external-library-linking` flag to include external libraries.
Make sure you have manually checked that the linked libraries are upgrade safe.
Warning: Potentially unsafe deployment of contracts/v2/platform/Factory.sol:Factory
You are using the `unsafeAllow.constructor` flag.
Warning: Potentially unsafe deployment of contracts/v2/periphery/Staking.sol:Staking
You are using the `unsafeAllow.constructor` flag.
Warning: Potentially unsafe deployment of contracts/v2/platform/BelongCheckIn.sol:BelongCheckIn
You are using the `unsafeAllow.external-library-linking` flag to include external libraries.
Make sure you have manually checked that the linked libraries are upgrade safe.
Warning: Potentially unsafe deployment of contracts/v2/platform/BelongCheckIn.sol:BelongCheckIn
You are using the `unsafeAllow.constructor` flag.
Warning: Potentially unsafe deployment of contracts/v2/periphery/Escrow.sol:Escrow
You are using the `unsafeAllow.constructor` flag.
✔ exploit: emergencyCancelPayment front-run + signature replay drains escrow (17892ms)
1 passing (22s)