Radar by SecureChain AI

Seam (SEAM) security report

Radar AI-scanned Seam on BNB Smart Chain. The verified source review found material risks involving external tax processing, dividend callbacks, and owner-controlled pool migration. Slither was not run.

The source does not expose an owner mint function after initialization, but it contains meaningful centralized-control and operational risks. The configured tax processor receives a maximum token allowance and may receive the contract's accumulated tax balance on processing failure. Transfers can also revert when the configured dividend contract rejects share updates, and the owner controls transitions that enable pool transfers.

Trust score: 54/100. Source verified: Yes. Chain: bsc. Contract: 0x23dcd2317a7d2d4602c74e6cd9273cb8e5e77777.

This report was last modified on 2026-10-15 and contains 4 material findings.

high finding: Tax processor can pull the contract's entire accumulated token balance

The tax processor is approved for an effectively unlimited amount of SEAM, and the failure path transfers the contract's full remaining SEAM balance to that processor. A compromised, malicious, or incorrectly configured tax processor could therefore divert accumulated tax tokens rather than merely process the current tax amount.

Evidence: In _processTax(), the contract sets allowance(address(this), taxProcessor) to type(uint256).max when the allowance is insufficient. If processTaxTokens(taxAmount) reverts, the catch block reads balanceOf(address(this)) and calls _plainTransfer(address(this), taxProcessor, remainingBalance), transferring the full contract balance to the configured processor. taxProcessor is assigned during initialize() and has no setter in this contract.

Recommendation: Use a bounded approval for the current tax amount, restrict the failure path to the current tax amount, and add explicit validation or governance controls for the tax processor. Consider pausing or quarantining failed liquidation instead of transferring all accumulated tokens to the external processor.

medium finding: Dividend callback failures can block ordinary token transfers

When dividendContract is configured, every transfer involving a non-exempt sender or recipient performs an external setShare callback. Any revert from that contract causes the entire token transfer to revert, creating a transfer-lock condition dependent on an external component.

Evidence: _afterTokenTransfer() calls IDividend(dividendContract).setShare(from, balanceOf(from)) and setShare(to, balanceOf(to)) without allowing callback failure. The catch blocks revert with DividendShareUpdateFailed. Only the token contract's address, zero address, dead address, dividend contract, and configured pools are skipped; ordinary transfers therefore depend on the dividend contract accepting both updates.

Recommendation: Do not make core ERC20 transfers depend on a reverting external dividend callback. Use non-blocking accounting, a failure queue, or an administrator-controlled recovery path with carefully limited authority. Validate the dividend contract during initialization.

medium finding: Owner-controlled migration can restrict transfers to configured pools

The owner controls the state transitions that determine whether transfers to or from configured pools are allowed. Before migration, transfers involving any configured pool revert, so an owner that delays or fails to complete migration can keep pool interactions unavailable.

Evidence: startMigration() and finalizeMigration() are both onlyOwner. In _transfer(), PoolState.BondingCurve requires !pools[from] && !pools[to], reverting with "Transfers to/from pools are restricted in BondingCurve state". The contract does not provide a permissionless or time-enforced transition out of BondingCurve.

Recommendation: Use an explicit, verifiable launch configuration and consider permissionless or time-bounded migration after required conditions are met. Publish the configured pool addresses and migration authority, and add safeguards against indefinite owner-controlled trading restriction.

medium finding: Unprotected external initializer requires atomic deployment initialization

initialize() is externally callable by any account that reaches the uninitialized proxy or clone first. The caller becomes the owner and receives the full initial supply, while also choosing the tax processor, dividend contract, pool list, and tax parameters. This is a deployment-time takeover risk if initialization is not performed atomically.

Evidence: initialize(InitParams memory params) is external and protected only by the generic initializer modifier; it does not restrict the caller or validate an expected deployer. It calls __Ownable_init(), which sets the owner to _msgSender(), and then mints maxSupply to msg.sender. The constructor disables initialization only on the implementation contract, not on a proxy or clone storage instance.

Recommendation: Initialize the proxy or clone in the same transaction as deployment, or use a factory that enforces initialization and verifies the resulting owner and configuration before exposure. Do not leave an instance externally callable while its initializer remains unused.