57676 sc high cross token accounting in receiver allows permanent freezing of erc20 royalty payouts
Submitted on: Oct 28th 2025 at 04:36:11 UTC by @Rhaydden for Audit Comp | Belong
Report ID: #57676
Report Type: Smart Contract
Report severity: High
Target: https://github.com/immunefi-team/audit-comp-belong/blob/feat/cairo/src/receiver/receiver.cairo
Impacts: Permanent freezing of unclaimed royalties
Description
Issue description
The Receiver contract mixes accounting across different ERC20 tokens when calculating pending payments, because total_released and released are global (not keyed by payment_token). As a result, inflating totals with a worthless ERC20 skews the computation for a valuable ERC20, causing oversized to_release amounts that exceed the Receiver’s balance in that valuable token and revert transfers.
Relevant excerpt from the code: https://github.com/immunefi-team/audit-comp-belong//blob/a17f775dcc4c125704ce85d4e18b744daece65af/src/receiver/receiver.cairo#L28-L31
// src/receiver/receiver.cairo
#[storage]
struct Storage {
payees: Vec<ContractAddress>,
shares: Map<ContractAddress, u16>,
total_released: u256, // BUG: not keyed by payment_token
released: Map<ContractAddress, u256>, // BUG: not keyed by payment_token
}
fn _release(
ref self: ContractState, payment_token: ContractAddress, to: ContractAddress,
) -> u256 {
let token = IERC20Dispatcher { contract_address: payment_token };
let total_released = self.total_released.read(); // uses global total
let released = self.released.read(to); // uses global per-payee
let to_release = self
._pending_payment(
to, token.balance_of(get_contract_address()) + total_released, released,
);
if to_release == 0 { return 0; }
self.released.write(to, released + to_release);
self.total_released.write(total_released + to_release);
token.transfer(to, to_release); // reverts if balance insufficient
...
}Because total_released and released are not tracked per token, the arithmetic blends “units” from multiple ERC20s. An attacker can:
Mint a huge amount of their own ERC20
Wto theReceiver.Call
release(W, payee)(anyone can callreleaseas long astois a valid payee).
This inflates global totals in W units. Later, releasing a valuable ERC20 A computes a very large to_release in A units, exceeding A balance and causing transfer to revert.
Impact
High — Permanent freezing of unclaimed royalties.
Releases in the valuable token can be permanently blocked after inflating global totals with a worthless token. Unblocking would require funding the Receiver with an equivalently huge amount of the valuable token, which is impractical, so affected royalties become effectively permanently frozen. No direct theft is required. This is effectively a DoS on payouts.
Recommended mitigation steps
Replace global accounting with per-token tracking:
total_released_by_token: Map<ContractAddress, u256>released_by_token: Map<(ContractAddress, ContractAddress), u256>(keyed by(payment_token, payee))
Update getters and interface:
totalReleased(payment_token: ContractAddress) -> u256released(payment_token: ContractAddress, account: ContractAddress) -> u256
Compute pending payment with per-token totals only:
to_release = f(token.balance_of(receiver) + total_released_by_token[token], released_by_token[(token, to)], shares[to])
Proof of Concept
The following PoC demonstrates how inflating global totals with a worthless token causes a release of a valuable token to compute an oversized to_release and fail due to insufficient balance.
Running the PoC
Run the test with:
Logs from the test run
The PASS indicates the expected panic (DoS condition) occurred during release(A, creator).
References
Vulnerable source file: https://github.com/immunefi-team/audit-comp-belong/blob/feat/cairo/src/receiver/receiver.cairo
Was this helpful?