57775 sc medium paytovenue will revert due to notenoughlongs funds in the escrow contract
Submitted on Oct 28th 2025 at 20:30:45 UTC by @Josh4324 for Audit Comp | Belong
Report ID: #57775
Report Type: Smart Contract
Report severity: Medium
Target: https://github.com/immunefi-team/audit-comp-belong/blob/main/contracts/v2/platform/BelongCheckIn.sol
Impacts:
Smart contract unable to operate due to lack of token funds
Description
Brief / Intro
The payToVenue function in the BelongCheckIn contract processes customer payments to venues, supporting both USDC and LONG tokens while applying platform subsidies, discounts, and promoter rewards. When a customer pays with LONG, the function attempts to distribute a platform subsidy from the venue's escrow balance. If the escrow lacks sufficient LONG tokens to cover the subsidy, the call reverts with a NotEnoughLONGs error. This creates a dependency on external escrow funding and can cause valid customer payments to fail. The root cause is missing pre-checks or fallback handling for insufficient escrow balances.
Vulnerability Details
Relevant excerpt from payToVenue:
function payToVenue(CustomerInfo calldata customerInfo) external {
BelongCheckInStorage memory _storage = belongCheckInStorage;
VenueRules memory rules = generalVenueInfo[customerInfo.venueToPayFor].rules;
_storage.contracts.factory.nftFactoryParameters().signerAddress.checkCustomerInfo(customerInfo, rules);
uint256 venueId = customerInfo.venueToPayFor.getVenueId();
if (customerInfo.promoter != address(0)) {
uint256 rewardsToPromoter = customerInfo.paymentInUSDC
? customerInfo.visitBountyAmount + customerInfo.spendBountyPercentage.calculateRate(customerInfo.amount)
: _storage.paymentsInfo.usdc
.unstandardize(
// standardization
_storage.paymentsInfo.usdc.standardize(customerInfo.visitBountyAmount)
+ customerInfo.spendBountyPercentage
.calculateRate(
_storage.paymentsInfo.long
.getStandardizedPrice(
_storage.contracts.longPF,
customerInfo.amount,
_storage.paymentsInfo.maxPriceFeedDelay
)
)
);
uint256 venueBalance = _storage.contracts.venueToken.balanceOf(customerInfo.venueToPayFor, venueId);
require(venueBalance >= rewardsToPromoter, NotEnoughBalance(rewardsToPromoter, venueBalance));
_storage.contracts.venueToken.burn(customerInfo.venueToPayFor, venueId, rewardsToPromoter);
_storage.contracts.promoterToken
.mint(customerInfo.promoter, venueId, rewardsToPromoter, _storage.contracts.venueToken.uri(venueId));
}
if (customerInfo.paymentInUSDC) {
_storage.paymentsInfo.usdc
.safeTransferFrom(customerInfo.customer, customerInfo.venueToPayFor, customerInfo.amount);
} else {
// platform subsidy - processing fee
uint256 subsidyMinusFees =
_storage.fees.platformSubsidyPercentage.calculateRate(customerInfo.amount)
- _storage.fees.processingFeePercentage.calculateRate(customerInfo.amount);
@> _storage.contracts.escrow
.distributeLONGDiscount(customerInfo.venueToPayFor, address(this), subsidyMinusFees);
// customer paid amount - longCustomerDiscountPercentage (3%)
uint256 longFromCustomer =
customerInfo.amount - _storage.fees.longCustomerDiscountPercentage.calculateRate(customerInfo.amount);
_storage.paymentsInfo.long.safeTransferFrom(customerInfo.customer, address(this), longFromCustomer);
uint256 longAmount = subsidyMinusFees + longFromCustomer;
if (rules.longPaymentType == LongPaymentTypes.AutoStake) {
// Approve only what is needed, then clear allowance after deposit.
_storage.paymentsInfo.long.safeApproveWithRetry(address(_storage.contracts.staking), longAmount);
_storage.contracts.staking.deposit(longAmount, customerInfo.venueToPayFor);
_storage.paymentsInfo.long.safeApprove(address(_storage.contracts.staking), 0);
} else if (rules.longPaymentType == LongPaymentTypes.AutoConvert) {
_swapLONGtoUSDC(customerInfo.venueToPayFor, longAmount);
} else {
_storage.paymentsInfo.long.safeTransfer(customerInfo.venueToPayFor, longAmount);
}
}
emit CustomerPaid(
customerInfo.customer,
customerInfo.venueToPayFor,
customerInfo.promoter,
customerInfo.amount,
customerInfo.visitBountyAmount,
customerInfo.spendBountyPercentage
);
}In the LONG payment branch the contract computes subsidyMinusFees and immediately calls:
Escrow.distributeLONGDiscount checks the venue's LONG deposits and reverts if the escrow has insufficient LONG balance:
Because payToVenue does not check the venue escrow balance before calling distributeLONGDiscount and has no fallback behavior, the entire payToVenue call reverts after partial execution when the escrow is underfunded.
Impact
Customers paying in LONG can experience unexpected transaction reverts when the associated venue escrow lacks sufficient LONG tokens to cover the subsidy. This prevents valid customer payments from completing, interrupting normal operation of the contract.
References
Vulnerable file: https://github.com/immunefi-team/audit-comp-belong/blob/main/contracts/v2/platform/BelongCheckIn.sol#L435
Proof of Concept (test)
Place this test into test/v2/platform/file.test.ts and run yarn test test/v2/platform/file.test.ts
Observed failing result:
-- End of report.
Was this helpful?