Severity: Medium
Component: ghost-dao-interface / src/containers/Bridge/BridgeCard.jsx
Commit tested: a911859
Summary
When the bridge amount is larger than the user’s GHST balance, BridgeCard.jsx clamps the value to the user’s full balance.
The problem is that the UI does not use that same value consistently.
The bridge transaction uses the clamped preparedAmount, while the "You will get" amount is calculated from the amount typed by the user.
For example, with a balance of 10 GHST and an input of 100 GHST:
Input: 100 GHST
Button disabled: false
"You will get": 99 GHST
Transaction amount: 10 GHST
The amount shown by the UI therefore does not match the amount passed to the bridge transaction.
Root cause
In src/containers/Bridge/BridgeCard.jsx:
const preparedAmount = useMemo(() => {
try {
const result = BigInt(parseFloat(amount) * Math.pow(10, 18));
if (result > ghstBalance._value) {
return ghstBalance._value;
}
return result;
} catch {
return 0n;
}
}, [amount]);
An amount greater than the user’s balance is replaced with the entire balance.
That value is then passed to the bridge call:
const txHash = await ghost(
chainId,
address,
convertedReceiver,
preparedAmount
);
However, amountAfterFee is calculated from the typed input rather than the clamped amount.
The balance check also compares the balance against preparedAmount. After the clamp, the value is equal to the balance, so an over-balance input is not rejected by that check.
Proof of Concept
I reproduced the component logic in a Node.js driver using the same BigInt wei representation. The wallet broadcast was captured instead of being sent.
Wallet balance: 10 GHST
Test 1
Typed amount: 100 GHST
Button disabled: false
"You will get": 99 GHST
Transaction sends: 10 GHST
Test 2
Typed amount: 1000 GHST
Button disabled: false
"You will get": 990 GHST
Transaction sends: 10 GHST
Control
Typed amount: 5 GHST
Button disabled: false
"You will get": 4.95 GHST
Transaction sends: 5 GHST
The behavior reproduced consistently across repeated runs.
Impact
An extra zero in the bridge amount is enough to cause the transaction to use the user’s entire GHST balance.
The main issue is the mismatch between the amount presented by the interface and the amount actually passed to the transaction. The user can see a result calculated from the amount they typed while the transaction uses a different amount.
Expected behavior
An amount greater than the available balance should be rejected, or the interface should update all displayed amounts to the exact value that will be submitted.
The amount shown in the confirmation flow should always correspond to the amount passed to the bridge transaction.
Suggested fix
The simplest fix is to remove the silent clamp and let the balance check reject over-balance input:
const preparedAmount = useMemo(() => {
try {
return BigInt(parseFloat(amount) * Math.pow(10, 18));
} catch {
return 0n;
}
}, [amount]);
Then the existing balance check can operate on the actual requested amount.
At minimum, if the clamp is intentional, the confirmation UI and fee calculation should use the same clamped amount that is actually sent.