Alchemix contest - AuditsAreUS's results

A protocol for self-repaying loans with no liquidation risk.

General Information

Platform: Code4rena

Start Date: 05/05/2022

Pot Size: $125,000 DAI

Total HM: 17

Participants: 62

Period: 14 days

Judge: leastwood

Total Solo HM: 15

Id: 120

League: ETH

Alchemix

Findings Distribution

Researcher Performance

Rank: 2/62

Findings: 4

Award: $14,655.61

🌟 Selected for report: 2

🚀 Solo Findings: 2

Findings Information

🌟 Selected for report: tintin

Also found by: 0xsomeone, AuditsAreUS, hyh

Labels

bug
duplicate
2 (Med Risk)

Awards

1164.4755 DAI - $1,164.48

External Links

Lines of code

https://github.com/code-423n4/2022-05-alchemix/blob/de65c34c7b6e4e94662bf508e214dcbf327984f4/contracts-full/AlchemicTokenV2Base.sol#L189-L191 https://github.com/code-423n4/2022-05-alchemix/blob/de65c34c7b6e4e94662bf508e214dcbf327984f4/contracts-full/AlchemicTokenV2Base.sol#L116-L121

Vulnerability details

Impact

It is possible for any whitelisted used to reduce totalMinted for themselves. This value is used in mint() to prevent a malicious minter from minting an infinite number of tokens.

By allowing a minter to reduce their own totalMinted they are able to perform infinite minting.

Proof of Concept

The attack by a malicious minter is to call mint() then lowerHasMinted() in a loop to mint as many tokens as desired.

The minter first calls mint() which will update their totalMinted to amount and mint amount to an arbitrary receiver address. They minter may set amount = mintCeiling[msg.sender] to maximise the amount they may mint each loop/

    uint256 total = amount + totalMinted[msg.sender];
    if (total > mintCeiling[msg.sender]) {
      revert IllegalState();
    }

    totalMinted[msg.sender] = total;

The malicious minter may then call lowerHasMinted to reduce totalMinted[msg.sender] = 0 for themselves.

  function lowerHasMinted(uint256 amount) external onlyWhitelisted {
    totalMinted[msg.sender] = totalMinted[msg.sender] - amount;
  }

The malicious minter may now call mint() again for mintCeiling[msg.sender] to mint more tokens for themselves. They then repeat calls to lowerHasMinted() and mint() until they have minted the desired number of tokens.

There are two mitigations for this, the first is to remove the function lowerHasMinted(). The other option is to make lowerHasMinted() an adminOnly function preventing minters from adjusting their owner amount.

#0 - 0xfoobar

2022-05-30T06:28:36Z

Duplicate of #166

Findings Information

🌟 Selected for report: AuditsAreUS

Labels

bug
2 (Med Risk)

Awards

6389.4401 DAI - $6,389.44

External Links

Lines of code

https://github.com/code-423n4/2022-05-alchemix/blob/de65c34c7b6e4e94662bf508e214dcbf327984f4/contracts-full/adapters/lido/WstETHAdapterV1.sol#L82-L84

Vulnerability details

Impact

The Lido adapter incorrectly calculates the price of WETH in terms of WstETH.

The function returns the price of WstETH in terms of stETH. The underlying token which we desire is WETH. Since stETH does not have the same value as WETH the output price incorrect.

The impact is severe as all the balance calculations require the price of the yield token converted to underlying. The incorrect price may over or understate the harvestable amount which is a core calculation in the protocol.

Proof of Concept

The function IWstETH(token).getStETHByWstETH() only converts WstETH to stETH. Thus, price() returns the value of WstETH in terms of stETH.

    function price() external view returns (uint256) {
        return IWstETH(token).getStETHByWstETH(10**SafeERC20.expectDecimals(token));
    }

Add extra steps to price() to approximate the rate for converting stETH to ETH. This can be done using the same curve pool that is used to convert stETH to ETH in unwrap().

#0 - 0xfoobar

2022-05-22T21:42:35Z

Sponsor disputed

The design mechanism relies upon stETH reaching eventual 1:1 redeemability for ETH after the merge and shanghai enables withdrawals. This is core to out like-kind collateral/asset model. We will update the stETH token adapter at that time to do direct redemptions instead of Curve swaps. So in the meantime, the protocol accrues discounted assets.

#1 - 0xleastwood

2022-06-08T14:41:08Z

While the sponsor's comments suggest that this will be a non-issue after the merge/shanghai enables withdrawals, I believe there is legitimacy in the fact that the protocol will accrue discounted assets. It does not lead to the loss of assets, but value can be leaked if WETH is priced incorrectly. As such, I'm downgrading this to medium severity.

Findings Information

🌟 Selected for report: AuditsAreUS

Labels

bug
2 (Med Risk)
sponsor confirmed

Awards

6389.4401 DAI - $6,389.44

External Links

Lines of code

https://github.com/code-423n4/2022-05-alchemix/blob/de65c34c7b6e4e94662bf508e214dcbf327984f4/contracts-full/AlchemistV2.sol#L1290-L1300 https://github.com/code-423n4/2022-05-alchemix/blob/de65c34c7b6e4e94662bf508e214dcbf327984f4/contracts-full/AlchemistV2.sol#L1268 https://github.com/code-423n4/2022-05-alchemix/blob/de65c34c7b6e4e94662bf508e214dcbf327984f4/contracts-full/AlchemistV2.sol#L1532 https://github.com/code-423n4/2022-05-alchemix/blob/de65c34c7b6e4e94662bf508e214dcbf327984f4/contracts-full/AlchemistV2.sol#L899 https://github.com/code-423n4/2022-05-alchemix/blob/de65c34c7b6e4e94662bf508e214dcbf327984f4/contracts-full/AlchemistV2.sol#L1625

Vulnerability details

Impact

It is possible for the contract to become stuck and unable to perform any actions if the totalShares of a yield token fall to zero while there is some pendingCredit still to be paid.

It will then be impossible to call deposit or withdraw functions, mints, burns, repay, liquidate, donate or harvest due to division by zero reverts in:

  • _distributeCredit()
  • _distributeUnlockedCredit()
  • _calculateUnrealizedDebt()
  • _convertSharesToYieldTokens()
  • donate()

Furthermore, any pendingCredit amount of tokens are still in the contract will become permanently stuck.

Proof of Concept

This case may arise under the follow steps a) deposit() is called by a user then time passes to earn some yield b) harvest() is called by the keeper which calls _distributeCredit() and increases pendingCredit c) withdraw() is called by the user to withdraw all funds

Since there is pendingCredit the following will have a non-zero balance for unlockedCredit however yieldTokenParams.totalShares is zero and thus we get a division by zero which reverts the entire transaction.

    function _distributeUnlockedCredit(address yieldToken) internal {
        YieldTokenParams storage yieldTokenParams = _yieldTokens[yieldToken];


        uint256 unlockedCredit = _calculateUnlockedCredit(yieldToken);
        if (unlockedCredit == 0) {
            return;
        }


        yieldTokenParams.accruedWeight     += unlockedCredit * FIXED_POINT_SCALAR / yieldTokenParams.totalShares;
        yieldTokenParams.distributedCredit += unlockedCredit;
    }

Each of the other listed functions will reach the same issue by attempting to divide some numerator by the totalShares which is zero.

Consider preventing totalShares from over becoming zero once it is set. That is enforce a user to leave at least 1 unit if they are the last user to withdraw.

Another option is to transfer the first 1000 shares to a "burn" account (e.g. 0x000...01), when the first user deposits.

Alternatively, when the last user withdraws, transfer all pending credit to this user and set the required variables to zero to replicate the state before any users have deposited.

#0 - 0xfoobar

2022-05-22T21:34:56Z

Sponsor confirmed

Disagree with severity because given the depth of distinct users using Alchemix, it is unlikely this scenario would occur.

#1 - 0xleastwood

2022-06-03T17:21:17Z

This is an interesting issue. At the moment, it sits somewhere between medium and high risk, so I will need to think about this more before coming to a decision.

#2 - 0xleastwood

2022-06-08T15:56:44Z

After further thought, I think this does not fit the criteria of high severity for the following reasons:

  • The protocol can be DoS'd on new deployments via front-running, but it does not lead to lost funds by users. It'd only require a new deployment by the Alchemist team.
  • If the protocol was to migrate to a new version of the protocol, a mass withdrawal event could lead to locked pendingCredit. However, because rewards are harvested by a keeper, I believe this to be unlikely as migrations will most certainly be coordinated by the protocol and its keepers. As such, users will be aware that they would miss out on rewards if the keeper does not harvest rewards prior to the migration.
  • Rewards are regularly harvested by the keeper, and as such, the value at risk is somewhat negligible.

For these reasons, I believe medium severity to be justified.

Fee on Transfer and Rebasing tokens are not accounted for in adapters or AlchemistV2

Lines of code

https://github.com/code-423n4/2022-05-alchemix/blob/de65c34c7b6e4e94662bf508e214dcbf327984f4/contracts-full/adapters/fuse/FuseTokenAdapterV1.sol#L66-L85 https://github.com/code-423n4/2022-05-alchemix/blob/de65c34c7b6e4e94662bf508e214dcbf327984f4/contracts-full/AlchemistV2.sol#L1319

Vulnerability details

Impact

Many ERC20 tokens are Fee on Transfer (FoT) in which a fee will be deducted from the amount transferred. The recipient will receive amount - fee tokens. Similarly rebasing tokens may change value up or down over time or during transfers.

If fee on transfer tokens are used in AlchemistV2 or the adapters the result is the contract may receive less tokens than sent in safeTranferFrom().

The impact of these tokens on the wrap() functions is that they will either a) spend the fee from the current contract balance b) revert if there is insufficient balance in the contract

Proof of Concept

AlchemistV2._wrap() will first transfer the tokens from the msg.sender to the current contract. It will then attempt to wrap this exact same amount of tokens. However if a fee has been applied during safeTransferFrom() then less than amount of tokens will be received by the contract and adapter.wrap() will either revert due to having insufficient balance or will spend the Alchemists tokens.

        TokenUtils.safeTransferFrom(underlyingToken, msg.sender, address(this), amount);
        uint256 wrappedShares = adapter.wrap(amount, address(this));
        if (wrappedShares < minimumAmountOut) {
            revert SlippageExceeded(wrappedShares, minimumAmountOut);
        }

FuseTokenAdapterV1.wrap() will transfer amount to this contract and then mint() the same amount in fuse. If they are FoT tokens the current contract will receive less than amount of tokens and therefore either mint() will fail due to insufficient balance or it will spend tokens from the contract which were not owned by the called.

    function wrap(
        uint256 amount,
        address recipient
    ) external onlyAlchemist returns (uint256) {
        SafeERC20.safeTransferFrom(underlyingToken, msg.sender, address(this), amount);
        SafeERC20.safeApprove(underlyingToken, token, amount);


        uint256 startingBalance = IERC20(token).balanceOf(address(this));


        uint256 error;
        if ((error = ICERC20(token).mint(amount)) != NO_ERROR) {
            revert FuseError(error);
        }


        uint256 endingBalance = IERC20(token).balanceOf(address(this));
        uint256 mintedAmount = endingBalance - startingBalance;


        SafeERC20.safeTransfer(token, recipient, mintedAmount);


        return mintedAmount;

The same principals also apply to YearnTokenAdpater.sol and VsperAdapterV1.sol.

Ensure there is sufficient documentation such that underlying tokens which are FoT or rebasing are not added to the protocol as underlying tokens or yield tokens.

#0 - 0xfoobar

2022-05-30T06:11:01Z

Sponsor acknowledged

Alchemix does not deal with fee-on-transfer or rebasing tokens

#1 - 0xleastwood

2022-06-03T17:09:39Z

As Alchemix does not deal with fee-on-transfer tokens, I'm inclined to mark this as QA because it assumes the protocol governance enlists a token type that is not compatible with the protocol's design.

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