57884 sc low staking tier manipulation via erc4626 shares slong

Submitted on Oct 29th 2025 at 11:21:47 UTC by @jo13 for Audit Comp | Belongarrow-up-right

  • Report ID: #57884

  • Report Type: Smart Contract

  • Report severity: Low

  • Target: https://github.com/immunefi-team/audit-comp-belong/blob/main/contracts/v2/platform/BelongCheckIn.sol

  • Impacts: Theft of unclaimed yield

Description

Brief / Intro

Tiers are determined using the staking vault’s ERC20 share balance (sLONG) via staking.balanceOf(addr), while tier thresholds are expressed in LONG assets (18 decimals). Because sLONG shares are freely transferable and their asset backing varies with the ERC4626 exchange rate, an attacker can temporarily inflate their share balance (for example, by borrowing or transferring in shares at call time) to qualify for a higher tier. This reduces platform fees or increases payouts, causing direct protocol revenue loss and unfair advantages in production.

Vulnerability Details

  • Tier lookups use share balance (not assets):

    // contracts/v2/platform/BelongCheckIn.sol
    // Venue tier lookup
    VenueStakingRewardInfo memory stakingInfo = stakingRewards[
        _storage.contracts.staking.balanceOf(venueInfo.venue).stakingTiers()
    ].venueStakingInfo;
    
    // Promoter tier lookup
    PromoterStakingRewardInfo memory stakingInfo = stakingRewards[
        _storage.contracts.staking
            .balanceOf(promoterInfo.promoter)
            .stakingTiers()
    ].promoterStakingInfo;
  • Tier thresholds expect LONG asset amounts (18 decimals):

    // contracts/v2/utils/Helper.sol
    /// @notice Resolves the staking tier based on the staked amount of LONG (18 decimals).
    function stakingTiers(uint256 amountStaked) external pure returns (StakingTiers tier) {
        if (amountStaked < 50000e18) {
            return StakingTiers.NoStakes;
        } else if (amountStaked >= 50000e18 && amountStaked < 250000e18) {
            return StakingTiers.BronzeTier;
        } else if (amountStaked >= 250000e18 && amountStaked < 500000e18) {
            return StakingTiers.SilverTier;
        } else if (amountStaked >= 500000e18 && amountStaked < 1000000e18) {
            return StakingTiers.GoldTier;
        }
        return StakingTiers.PlatinumTier;
    }
  • ERC4626 shares vs assets distinction (shares are transferable):

    // contracts/v2/periphery/Staking.sol
    contract Staking is Initializable, ERC4626, Ownable {
        // Deposit mints shares; share-based bookkeeping
        function _deposit(address by, address to, uint256 assets, uint256 shares) internal override {
            super._deposit(by, to, assets, shares);
            stakes[to].push(Stake({shares: shares, timestamp: block.timestamp}));
        }
    
        // Clear separation of assets and shares
        function emergencyWithdraw(uint256 assets, address to, address _owner) external returns (uint256 shares) {
            if (assets > maxWithdraw(_owner)) revert WithdrawMoreThanMax();
            shares = previewWithdraw(assets);   // convert assets -> shares
            _emergencyWithdraw(msg.sender, to, _owner, assets, shares);
        }
    }
    • balanceOf(address) returns the sLONG share balance (ERC20), not LONG assets.

    • Shares are freely transferable and their asset backing changes with reward distributions (exchange-rate effect).

  • Where tiers impact economics:

    • Venue deposit fees:

      // contracts/v2/platform/BelongCheckIn.sol
      uint256 platformFee = stakingInfo.depositFeePercentage.calculateRate(venueInfo.amount);
    • Promoter settlement fees:

      // contracts/v2/platform/BelongCheckIn.sol
      uint24 percentage = promoterInfo.paymentInUSDC
          ? stakingInfo.usdcPercentage
          : stakingInfo.longPercentage;
      uint256 platformFees = percentage.calculateRate(toPromoter);
  • Exploitable patterns (how manipulation works):

    • Share-transfer spoof: Move/borrow sLONG shares into the address just-in-time, call the fee-sensitive function (venueDeposit / distributePromoterPayments), then move them back.

    • Exchange-rate gaming: Deposit before a large distributeRewards() to mint more shares per asset; since tiers read shares, user qualifies for higher tiers with fewer underlying LONG than intended.

Impact Details

  • Direct revenue loss

    • Venues can momentarily appear in higher tiers to reduce depositFeePercentage on large deposits.

    • Promoters can reduce usdcPercentage/longPercentage fees taken from their settlements by inflating tier at call time.

  • Systemic unfairness

    • Tiers are meant to reflect sustained asset commitment; using transferable shares allows benefits without genuine or time-weighted stake.

  • Severity mapping (Immunefi scope)

    • High – Theft of unclaimed yield/royalties (platform fee leakage across deposits/settlements).

References

chevron-rightReferenced files and locationshashtag
  • Tier lookups using shares:

    • contracts/v2/platform/BelongCheckIn.sol (venue and promoter tier lookups using staking.balanceOf(...).stakingTiers()).

  • Tier thresholds expect assets:

    • contracts/v2/utils/Helper.solstakingTiers(uint256 amountStaked) with 18-decimal LONG thresholds.

  • ERC4626 shares vs assets:

    • contracts/v2/periphery/Staking.sol (inherits ERC4626; balanceOf is shares; previewWithdraw/convertToAssets separate assets).

  • Fee application points:

    • contracts/v2/platform/BelongCheckIn.soldepositFeePercentage.calculateRate(...), usdcPercentage/longPercentage.calculateRate(...).

Proof of Concept

The following test demonstrates manipulating the venue deposit fee by temporarily transferring sLONG shares to the venue address prior to calling venueDeposit, then returning them after the call.

Add this test to /home/jo/audit-comp-belong/test/v2/platform/belong-check-in.test.ts and run:

LEDGER_ADDRESS=0x0000000000000000000000000000000000000001 PK=0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef npx hardhat test test/v2/platform/belong-check-in.test.ts --grep "Staking tier manipulation"

  • Use asset-equivalent balances when evaluating tiers. For ERC4626 vaults this means converting shares to assets using the vault’s conversion function (e.g., convertToAssets(balanceOf(addr)) or previewRedeem(balanceOf(addr))) before passing to stakingTiers(...).

  • Alternatively, track time-weighted or non-transferable stake accounting (e.g., maintain internal staking records that reflect lockups or non-transferable positions) if tiers should represent committed stakes.

Was this helpful?