uint64 Overflow in Cross-Chain Bridge Silently Destroys Solana Withdrawals
The Risk
This is a smart contract finding on a cross-chain bridge. The bridge takes deposits on one blockchain and releases them on another. Because of a single line of buggy arithmetic, any user who withdraws more than about 18 tokens from the bridge to the Solana side will lose most of their money. The source account is emptied in full, but only a tiny leftover reaches the destination. There is no error, no warning, no refund, and no way to recover the difference. When the Solana path goes live, the first user to try a real withdrawal is the first to find the bug.
The Vulnerability
The bridge's internal accounting uses a 256-bit unsigned integer for the withdrawal amount on the source chain. The function that packs the cross-chain message for Solana downcasts that amount to a 64-bit unsigned integer without any bounds check, no require statement, no SafeCast wrapper:
bytes8 slot3 = bytes8(uint64(_xfer.quantity)); Solidity performs modular reduction on the cast, so any value above type(uint64).max wraps around silently. For an 18-decimal token, type(uint64).max is worth roughly 18.44 tokens. Every withdrawal above that threshold is corrupted. The same path to an EVM chain packs the full 256-bit quantity into a 32-byte slot and is unaffected, which proves the bug is specific to the Solana encoding path.
The Attack
No attacker is required. This is a self-inflicted vulnerability that fires on normal user activity. The full call path, from the user calling the withdrawal function through the portfolio contract, through the bridge router, and into the Solana packer, does not validate the quantity against the 64-bit bound at any step. The one intermediate function that looks like a bounds guard is actually a decimal-place truncator that is a no-op when the source and destination token decimals match.
Crucially, the source-chain balance is deducted in full before the cross-chain message is packed. The user pays the full withdrawal amount on the source side. Then the bridge packer wraps the amount down to a small value and encodes that into the outbound message. The difference between what was deducted and what was encoded is permanently unrecoverable. There is no revert, no emitted error event, and no refund path.
Reproduction
An end-to-end Foundry test reproduces the full flow: calling the withdrawal function with 100 tokens deducts 100 tokens from the user's portfolio, but the packed cross-chain message contains roughly 7.77 tokens, a 92% loss. A second test pins an exact power-of-two withdrawal that packs to zero, a 100% loss. A control test to an EVM destination shows zero loss on the same amount, confirming the bug is isolated to the Solana encoding.
The Impact
| Withdrawal | Delivered to Solana | Permanent Loss |
|---|---|---|
| 20 tokens | ~1.55 tokens | 92% |
| 100 tokens | ~7.77 tokens | 92% |
| 1,000 tokens | varies | >99% |
| 264 wei | 0 | 100% |
The bridge's Solana path was not enabled in production at the time of the report, but the vulnerable bytecode was fully compiled into the deployed contract and reachable via a single privileged administrative call. Once enabled, every realistic withdrawal to Solana silently loses most of its value. The team would discover the bug only from user complaints after the damage.
Remediation
Add an explicit bounds check before the downcast:
require(_xfer.quantity <= type(uint64).max, "PB-QOF-01"); Or use OpenZeppelin's SafeCast helper:
bytes8 slot3 = bytes8(SafeCast.toUint64(_xfer.quantity)); Additional hardening steps:
- Apply SafeCast or an equivalent bounds check everywhere the contract narrows a
uint256into a smaller integer type. Audit the full contract surface, not just the Solana packer. - Emit a distinct revert error when an out-of-range withdrawal is attempted, so integrators can surface a meaningful message to users instead of a silent failure.
- Add a test harness that fuzzes withdrawal quantities around the 64-bit boundary as a permanent regression check.
- Before enabling any new destination chain, run the same packing functions against the full range of in-scope token denominations and confirm zero loss.