善行计划 (善行) security report
Radar by SecureChain AI scanned the verified 善行计划 contract on BNB Smart Chain. Findings include excessive-tax lock conditions, external tax-processor trust, dividend callback failures, and owner-controlled migration.
Material risks were identified. The most significant are the ability to configure buy or sell tax rates above 100%, which can make taxed pool transfers revert, and the dependency on an external dividend contract whose failed callback reverts token transfers. Slither was skipped, so no Slither findings are available.
Trust score: 42/100. Source verified: Yes. Chain: bsc. Contract: 0xaf9a56ad31593ed423ec7916b4e40a7cce5b7777.
This report was last modified on 2026-10-08 and contains 5 material findings.
high finding: Initialization permits tax rates above 100%, enabling pool-transfer lock conditions
The initializer accepts buyTax and sellTax as uint16 values but does not require either value to be at most 10000 basis points. When a tax rate exceeds 10000, _taxedTransfer calculates remainingAmount = amount - tax and reverts because tax is greater than amount. Once the owner moves the token into a tax-enforced state, affected buys or sells involving configured pools can therefore become unexecutable.
Evidence: initialize() stores params.buyTax and params.sellTax directly in poolState without an upper-bound check. _getTaxWithPoolState() computes (amount * currentBuyTaxRate) / 10000 or (amount * currentSellTaxRate) / 10000, and _taxedTransfer() executes uint256 remainingAmount = amount - tax. startMigration() and finalizeMigration() are onlyOwner functions that control entry into the tax-enforced states.
Recommendation: Validate both tax parameters with require(rate <= 10000) during initialization. Consider enforcing a documented lower maximum and adding tests for buys and sells at the configured rates before enabling migration.
high finding: External tax processor receives unlimited token allowance and can pull the contract's token balance
During the first tax liquidation, the token grants taxProcessor an unlimited allowance over the token contract's balance. The configured processor can subsequently use transferFrom to move tokens held by the token contract, including accumulated tax tokens and any other tokens sent or held there. This creates a powerful external trust dependency and a potential loss path if the processor is malicious, compromised, or incorrectly configured.
Evidence: _processTax() calls _approve(address(this), taxProcessor, type(uint256).max) when the allowance is insufficient. The approval is not reduced after processing. If processing reverts, the catch block calls _plainTransfer(address(this), taxProcessor, remainingBalance), transferring the entire remaining token balance to taxProcessor. taxProcessor is set from params.taxProcessor during initialize().
Recommendation: Approve only the exact taxAmount, reset the allowance after processing, and strictly limit processor withdrawals to the intended tax amount. Add recipient and processor validation, and document the processor as a privileged external dependency.
medium finding: Dividend callback failures can halt ordinary token transfers
If dividendContract is configured, transfers involving non-excluded accounts synchronously call setShare for the sender and recipient. Any revert from that external contract is converted into DividendShareUpdateFailed and reverts the entire token transfer. A malfunctioning, incompatible, or deliberately reverting dividend contract can therefore create a material transfer-availability failure.
Evidence: _afterTokenTransfer() calls IDividend(dividendContract).setShare(from, balanceOf(from)) and setShare(to, balanceOf(to)) unless the address is excluded. Both calls are wrapped in try/catch blocks that revert with DividendShareUpdateFailed on failure. dividendContract is assigned during initialize().
Recommendation: Avoid making accounting callbacks capable of reverting core ERC20 transfers, or use a failure-tolerant mechanism with bounded gas and recoverable queues. Validate the dividend contract during initialization and provide a controlled way to disable or replace a failed integration.
medium finding: Owner-controlled migration gates pool access and tax activation
The owner controls when the token leaves BondingCurve and enters the tax-enforced lifecycle. In BondingCurve state, transfers to or from every configured pool revert. The owner can also delay migration indefinitely, while finalizeMigration activates anti-farmer and later tax enforcement using the configured rates and durations. This is a centralized launch and transfer-control risk rather than proof of an exploit.
Evidence: _transfer() requires !pools[from] && !pools[to] in PoolState.BondingCurve, reverting with "Transfers to/from pools are restricted in BondingCurve state". startMigration() and finalizeMigration() are both external onlyOwner functions and change poolState.state.
Recommendation: Use immutable or governance-controlled launch parameters with transparent timelocks, publish the intended migration schedule, and consider removing unilateral owner control over state transitions after deployment.
medium finding: Tax expiration is only processed when a transfer targets mainPool
Tax expiration is not evaluated on every transfer or by a keeper-independent state transition. _liquidateTax() is called from _transfer(), but its expiration logic runs only when to == mainPool. If no valid transfer targets mainPool, the contract can remain in a tax-enforced state beyond the configured expiration and continue applying pool taxes.
Evidence: _liquidateTax(address to) enters its state-transition logic only when currentState is tax-enforced, notLiquidating is true, and to == mainPool. The tax expiration check then changes the state to TaxFree and zeroes the rates. Transfers involving other configured pools do not satisfy this trigger condition.
Recommendation: Make expiration enforcement independent of the destination, such as through a public permissionless state-sync function or checks in all relevant transfer paths. Add tests covering expiration when mainPool is inactive or unreachable.