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 | Belongarrow-up-right

  • 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

circle-exclamation

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?