Open code423n4 opened 2 years ago
The amount would be miniscule due to the initial share amount scale being equal to the staked tokens scale (1e6), but indeed, the attack could be applied infinite times, though it's likely then that the gas costs incurred would exceed the profits gained.
Lines of code
https://github.com/code-423n4/2022-02-anchor/blob/7af353e3234837979a19ddc8093dc9ad3c63ab6b/contracts/anchor-token-contracts/contracts/gov/src/staking.rs#L88
Vulnerability details
Impact
The staking contract keeps track of shares of each user. When withdrawing from the staking contract the
amount
parameter is converted toshares
and this value is decreased (shares = amount / total_balance * total_share
). This shares calculation rounds down which allows stealing tokens.The current code seems to try to protect against this by doing a maximum
std::cmp::max(v.multiply_ratio(total_share, total_balance).u128(), 1u128)
but this only works for the case where the shares would round down to zero. It's still exploitable.POC
Assume
total_balance = 2e18
andtotal_share=1e18
. Then withdrawing/burning one share should allow withdrawingtotal_balance / total_share = 2
amount. However, imagine someone owns 1 shares (bought for 2 amount). Thenwithdraw_voting_tokens(amount=3)
. Andwithdraw_share = std::cmp::max(v.multiply_ratio(total_share, total_balance).u128(), 1u128) = max(amount=3 * total_share=1e18 / total_balance=2e18, 1) = max("3/2", 1) = 1
The attacker made a profit.The attacker can make a profit by staking the least
amount
to receive one share, and then immediately withdrawing this one share by specifying the largestamount
such that the integer rounding still rounds to this one share. They make a small profit. They can repeat this many times in a single transaction with many messages. Depending on the token price, this can be profitable as similar attacks show.Recommended Mitigation Steps
The protocol tries to protect against this attack but the
max(., 1)
is not enough mitigation. There are several ways to fix this:withdraw_share
withdraw_amount
with the newwithdraw_share
even if theamount
parameter was set.share
parameter (instead of theamount
parameter) and use it aswithdraw_share
. Rounding down on the resultingwithdraw_amount
is bad for the attacker as burning a share leads to fewer tokens.