Althea Liquid Infrastructure - 0xpiken's results

Liquid Infrastructure.

General Information

Platform: Code4rena

Start Date: 13/02/2024

Pot Size: $24,500 USDC

Total HM: 5

Participants: 84

Period: 6 days

Judge: 0xA5DF

Id: 331

League: ETH

Althea

Findings Distribution

Researcher Performance

Rank: 35/84

Findings: 2

Award: $87.74

🌟 Selected for report: 0

🚀 Solo Findings: 0

Lines of code

https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/main/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L127-L146

Vulnerability details

Impact

Most functions of LiquidInfrastructureERC20 could potentially be susceptible to a DoS (Denial of Service) attack if the size of holders experiences a significant increase.

Proof of Concept

When the distribution is not in progress (LockedForDistribution is false), approved holders of LiquidInfrastructureERC20 can freely transfer tokens among themselves without restriction. The recipient will be added to the holders list if their previous balance was zero:

142:        bool exists = (this.balanceOf(to) != 0);
143:        if (!exists) {
144:            holders.push(to);
145:        }

All holders listed in holders are eligible to receive future reward distributions. However, it doesn't check if the recipient has been added to holders before, a malicious user can increase the size of holders significantly by calling LiquidInfrastructureERC20#transfer() multi times to transfer zero amount of tokens to an approved holder with a zero balance. Functions that iterate over holders may encounter "Out of Gas" errors and revert:

  • token transfer
  • token burning
  • token minting
  • reward distribution

Let's add a function in LiquidInfrastructureERC20 to retrive the size of holders:

    function lengthOfHolders() public view returns (uint256) {
        return holders.length;
    }

Then copy below codes to liquidERC20.ts and run npx hardhat test:

  it("Increase holders", async function () {
    const { infraERC20, holder1, holder2, badSigner } =await liquidErc20Fixture();
    await infraERC20.approveHolder(holder1.address);
    await infraERC20.approveHolder(holder2.address);
    expect(await infraERC20.lengthOfHolders()).to.equal(0);
    for (let i=0; i<256; i++) {
      await infraERC20.connect(holder1).transfer(holder2.address, 0);
    }
    expect(await infraERC20.lengthOfHolders()).to.equal(256);
  });

The size of holders can be easily increased to 256 through 256 times of zero token transferring.

Tools Used

Manual review

  • Use EnumerableSet.AddressSet provided by openzeppelin to manage holders.
  • to can only be added into holders when pervious balance is zero and amount is not zero.

Assessed type

DoS

#0 - c4-pre-sort

2024-02-22T06:47:24Z

0xRobocop marked the issue as duplicate of #77

#1 - c4-judge

2024-03-04T13:23:44Z

0xA5DF marked the issue as satisfactory

#2 - c4-judge

2024-03-08T15:08:02Z

0xA5DF changed the severity to 3 (High Risk)

Awards

80.5583 USDC - $80.56

Labels

bug
2 (Med Risk)
satisfactory
duplicate-703

External Links

Lines of code

https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/main/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L275

Vulnerability details

Impact

The distribution reward an approved holder received might be less than expected when some of other holders are disapproved.

Proof of Concept

Whenever the distribution is ready, any one can distribute token revenue to the eligible Liquid Infrastructure ERC20 holders by calling LiquidInfrastructureERC20#distribute(). For a specific token, the reward per token will be calculated firstly:

269:        // Calculate the entitlement per token held
270:        uint256 supply = this.totalSupply();
271:        for (uint i = 0; i < distributableERC20s.length; i++) {
272:            uint256 balance = IERC20(distributableERC20s[i]).balanceOf(
273:                address(this)
274:            );
275:            uint256 entitlement = balance / supply;
276:            erc20EntitlementPerUnit.push(entitlement);
277:        }

Then the total reward will be calculated and distributed to the Liquid Infrastructure ERC20 holders:

214:        for (i = nextDistributionRecipient; i < limit; i++) {
215:            address recipient = holders[i];
216:            if (isApprovedHolder(recipient)) {
217:                uint256[] memory receipts = new uint256[](
218:                    distributableERC20s.length
219:                );
220:                for (uint j = 0; j < distributableERC20s.length; j++) {
221:                    IERC20 toDistribute = IERC20(distributableERC20s[j]);
222:@>                  uint256 entitlement = erc20EntitlementPerUnit[j] *
223:@>                      this.balanceOf(recipient);
224:@>                  if (toDistribute.transfer(recipient, entitlement)) {
225:                        receipts[j] = entitlement;
226:                    }
227:                }
228:
229:                emit Distribution(recipient, distributableERC20s, receipts);
230:            }
231:        }

If a holder is disapproved, they are not eligible to receive any rewards no matter how many Liquid Infrastructure ERC20 tokens they holds.

However, the token balances of disapproved holders are not excluded from supply when calculating entitlement. The calculated entitlement will be diluted and the reward an approved holder received is less than expected. Below is an example:

  • Both Alice and Bob hold 200 Liquid Infrastructure tokens
  • The totalSupply of Liquid Infrastructure token is 400
  • One of distribution reward is 100 USDC
  • Bob is disapproved by the owner
  • Alice distributeToAllHolders() to distribute USDC reward
    • entitlement is 0.25 USDC (100 / 400)
    • Alice receives 50 USDC (0.25 * 200)
    • The remaining 50 USDC is still locked in the contract

Copy below codes to liquidERC20.ts and run npx hardhat test:

  it("The distribution reward is diluted", async function () {
    const {
      infraERC20,
      erc20Owner,
      testERC20A,
      testERC20B,
      testERC20C,
      nftAccount1,
      nftAccount2,
      nftAccount3,
      holder1,
      holder2,
      holder3,
      holder4,
    } = await liquidErc20Fixture();

    const holders = [holder1, holder2, holder3, holder4];
    for (let holder of holders) {
      await infraERC20.approveHolder(holder.address);
      await infraERC20.mint(holder.address, 100);
    }
    const nft = await deployLiquidNFT(nftAccount1);
    const erc20s: ERC20[] = [testERC20A];
    await nft.setThresholds(erc20s, erc20s.map(() => 0));
    await nft.transferFrom(nftAccount1.address, await infraERC20.getAddress(), (await nft.AccountId()));
    await testERC20A.transfer(await nft.getAddress(), 30000);
    expect(await testERC20A.balanceOf(nft.getAddress())).to.equal(30000);
    await infraERC20.addManagedNFT(nft.getAddress());
    await infraERC20.withdrawFromAllManagedNFTs();
    expect(await testERC20A.balanceOf(nft.getAddress())).to.equal(0);
    expect(await testERC20A.balanceOf(infraERC20.getAddress())).to.equal(30000);
    const holder1BalanceBeforeDistribution = await testERC20A.balanceOf(holder1.getAddress());
    const holder2BalanceBeforeDistribution = await testERC20A.balanceOf(holder2.getAddress());
    const holder3BalanceBeforeDistribution = await testERC20A.balanceOf(holder3.getAddress());
    const holder4BalanceBeforeDistribution = await testERC20A.balanceOf(holder4.getAddress());
    //@audit-info disapprove holder4 before distribution
    await infraERC20.disapproveHolder(holder4.address);
    mine(500);
    await infraERC20.distributeToAllHolders();
    const holder1BalanceAfterDistribution = await testERC20A.balanceOf(holder1.getAddress());
    const holder2BalanceAfterDistribution = await testERC20A.balanceOf(holder2.getAddress());
    const holder3BalanceAfterDistribution = await testERC20A.balanceOf(holder3.getAddress());
    const holder4BalanceAfterDistribution = await testERC20A.balanceOf(holder4.getAddress());
    //@audit-info all approved holders only received 7500 of reward
    expect(holder1BalanceAfterDistribution - holder1BalanceBeforeDistribution).to.equal(7500);
    expect(holder2BalanceAfterDistribution - holder2BalanceBeforeDistribution).to.equal(7500);
    expect(holder3BalanceAfterDistribution - holder3BalanceBeforeDistribution).to.equal(7500);
    //@audit-info 7500 of reward is locked in infraERC20
    expect(holder4BalanceAfterDistribution - holder4BalanceBeforeDistribution).to.equal(0);
    expect(await testERC20A.balanceOf(infraERC20.getAddress())).to.equal(7500);
  });

Tools Used

Manual review

All Liquid Infrastructure ERC20 token balances of disapproved holders should be excluded from supply when calculating entitlement.

Assessed type

Math

#0 - c4-pre-sort

2024-02-20T05:25:32Z

0xRobocop marked the issue as duplicate of #703

#1 - c4-judge

2024-03-04T14:37:00Z

0xA5DF marked the issue as satisfactory

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter