57296 sc high retroactive referral tier underpayment in royaltiesreceiverv2 due to dynamic shares applied to historical funds

Submitted on Oct 25th 2025 at 03:18:25 UTC by @Rhaydden for Audit Comp | Belongarrow-up-right

  • Report ID: #57296

  • Report Type: Smart Contract

  • Report severity: High

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

  • Impacts:

    • Theft of unclaimed royalties

Description

Issue description

RoyaltiesReceiverV2 computes each payee’s owed amount using the current shares at the time of release, not the shares that applied when funds arrived. Referral shares depend on the creator’s current tier which is derived from usedCode[creator][code]. When the creator uses the same referral code again (deploying another collection), the tier changes and shares() yields a new split. _pendingPayment retroactively applies this new split to all historical funds causing misallocation.

This dynamic shares logic (tied to current factory parameters and current referral tier) is implemented here:

https://github.com/immunefi-team/audit-comp-belong//blob/a17f775dcc4c125704ce85d4e18b744daece65af/contracts/v2/periphery/RoyaltiesReceiverV2.sol#L107-L132

function shares(address account) public view returns (uint256) {
        RoyaltiesReceivers memory _royaltiesReceivers = royaltiesReceivers;

        Factory _factory = factory;
        Factory.RoyaltiesParameters memory royaltiesParameters = _factory.royaltiesParameters();
        if (account == _royaltiesReceivers.creator) {
            return royaltiesParameters.amountToCreator;
        } else {
            uint256 platformShare = royaltiesParameters.amountToPlatform;
            uint256 referralShare;
            if (_royaltiesReceivers.referral != address(0)) {
                referralShare = _factory.getReferralRate(
                    _royaltiesReceivers.creator, referralCode, royaltiesParameters.amountToPlatform
                );

                if (referralShare > 0) {
                    unchecked {
                        platformShare -= referralShare;
                    }
                }
            }
            return account == _royaltiesReceivers.platform
                ? platformShare
                : account == _royaltiesReceivers.referral ? referralShare : 0;
        }
    }

The retroactive PaymentSplitter math applies the current share to total historical receipts here:

https://github.com/immunefi-team/audit-comp-belong//blob/a17f775dcc4c125704ce85d4e18b744daece65af/contracts/v2/periphery/RoyaltiesReceiverV2.sol#L227-L238

Tier changes when the creator uses the same referral code again here:

https://github.com/immunefi-team/audit-comp-belong//blob/a17f775dcc4c125704ce85d4e18b744daece65af/contracts/v2/platform/extensions/ReferralSystemV2.sol#L157-L165

The factory increments usage during collection creation in factory.sol:

https://github.com/immunefi-team/audit-comp-belong//blob/a17f775dcc4c125704ce85d4e18b744daece65af/contracts/v2/platform/Factory.sol#L254

Impact

High - Theft of unclaimed royalties

When the referral tier decreases later, the contract underpays the referral and overpays the platform on historical funds that were already in the receiver but unclaimed. The creator’s share remains correct. The misallocation occurs between referral and platform.

Example with default config (creator 80%, platform 20%; referral tiers: 50% -> 30% of platform share):

  • Two equal inflows: 1000 ETH + 1000 ETH

  • Correct per-period:

    • Referral: 100 + 60 = 160

    • Platform: 100 + 140 = 240

    • Creator: 800 + 800 = 1600

  • Actual with retroactive dynamic shares (after tier drops to 30% before release):

    • Referral: 2000 × 6% = 120 (underpaid by 40)

    • Platform: 2000 × 14% = 280 (overpaid by 40)

    • Creator: 2000 × 80% = 1600

Snapshot shares per receiver at initialization. Store creator/platform/referral shares once during initialize() (based on then-current tier and royalties parameters) and make shares() return those stored constants. This follows v1 behavior and removes retroactive drift.

Proof of Concept

Paste the PoC below in test/v2/periphery/royalties-receiver-v2.poc.test.ts:

Run with:

Logs

The test asserts:

  • creator: 1600 ETH

  • platform: 280 ETH

  • referral: 120 ETH

  • total: 2000 ETH

  • Deltas vs correct: platform +40, referral −40

Was this helpful?