Yield Witch v2 contest - hickuphh3's results

Fixed-rate borrowing and lending on Ethereum

General Information

Platform: Code4rena

Start Date: 14/07/2022

Pot Size: $25,000 USDC

Total HM: 2

Participants: 63

Period: 3 days

Judge: PierrickGT

Total Solo HM: 1

Id: 147

League: ETH

Yield

Findings Distribution

Researcher Performance

Rank: 4/63

Findings: 2

Award: $401.22

๐ŸŒŸ Selected for report: 1

๐Ÿš€ Solo Findings: 0

Awards

381.8352 USDC - $381.84

Labels

bug
QA (Quality Assurance)
sponsor confirmed

External Links

Codebase Impressions & Summary

Functionality

The revised witch (liquidation engine) contract includes the following improvements over the previous version. As stated in the README, they are:

  1. Greater flexibility in exploring different liquidation models.
  2. Making liquidations more profitable for liquidators by allowing payments in fyToken.
  3. Introduce a mechanism to reward starting an auction.
  4. Allow fine-tuning of all parameters for any collateral/underlying pair.
  5. Correct bugs.

The liquidations flow was quite easy to follow as it consists of the following:

  1. Liquidation parameters are defined by governance functions (auction duration, vault proportion, auctioneer reward etc.)
  2. Starting an auction: auction()
  3. Liquidators executing the liquidations: payBase() and payFYToken()
  4. Either the entire vault collateral has been auctioned off, or cancel() is called to prematurely end the auction

Documentation

The README was very extensive and thorough, and succinctly explained design considerations made. Flow diagrams were provided to help visualise the interactions required between different contracts. Inline comments were appropriate too, aided in understanding the functionality.

Tests

All foundry tests passed as expected. One area of improvement is to have mainnet forking tests, since mocking is used for the external contracts. Running forge coverage unfortunately didnโ€™t work. I suspect it is due to the instability of the feature rather than the fault of the tests.

Gas Optimizations

Casting could be avoided if input / output params were defined appropriately. For instance, inkOut, artIn in _updateAccounting(), and liquidatorCut and auctioneerCut could have been uint128 instead of uint256.

Low Severity Findings

L01: Vaults that are over-collateralised after partial liquidation are possibly subject to further liquidations

Description

If a vault becomes over-collateralised after a partial liquidation, it is still subject to further liquidation as the auction isnโ€™t closed. The vault owner has to call cancel() himself, or trust other altruistic actors to perform this action on his behalf. Liquidators will unlikely do it because they are economically incentivised not to do so.

One can however argue that this is mitigated by the fact that protocol (governance) sets the vault proportion that can be auctioned. Regardless of whether the fact that the vault is over-collateralised after partial liquidations, the liquidators arguably are given the right to carry out further liquidations up to the proportion set.

Nevertheless, a reason for a revised liquidations witch contract is that โ€œMore often than not, liquidated users have lost all their collateral as we have failed to make liquidations competitive.โ€. Hence, it might make sense to ensure that users need not lose more collateral than necessary.

Consider checking if the vault is over-collateralized (maybe in _updateAccounting()) and close the auction if it is. This however adds complexity to the liquidation logic, as you have to update the cauldron first cauldron.slurp() before checking and updating the collateralization status. It will also break the CEI pattern, which might be unfavourable.

L02: Comparison in _calcPayout() should include equality

Line References

https://github.com/code-423n4/2022-07-yield/blob/6ab092b8c10e4dabb470918ae15c6451c861655f/contracts/Witch.sol#L586

TLDR

- else if (elapsed > duration)
+ else if (elapsed >= duration)

Description

In the case where elapsed == duration, proportionNow evaluates to 1e18, which is the same result when elapsed > duration. Proof below.

proportionNow =
  uint256(initialProportion) +
  uint256(1e18 - initialProportion).wmul(elapsed.wdiv(duration));

// = initialProportion + (1e18 - initialProportion).wmul(1e18)
// = initialProportion + (1e18 - initialProportion) * 1e18 / 1e18
// = initialProportion + 1e18 - initialProportion
// = 1e18

Of slightly greater importance, this handles the edge case when elapsed = duration = 0, ie. the liquidation transaction is included in the same block / has the same timestamp as the auction initialization transaction

As per the TLDR.

P.S. Regarding zero duration auctions

Since the proportion given for zero duration auctions is 1e18, it is equivalent to an auction of infinite duration with 100% initial offer: duration == type(uint32).max and line_.initialOffer = 1e18.

L03: Incorrect description for auctioneerCut

Line References

https://github.com/code-423n4/2022-07-yield/blob/6ab092b8c10e4dabb470918ae15c6451c861655f/contracts/Witch.sol#L284

https://github.com/code-423n4/2022-07-yield/blob/6ab092b8c10e4dabb470918ae15c6451c861655f/contracts/Witch.sol#L342

Description

Technically, the auctioneerCut goes to the to address specified by the auctioneer when auction() is called, which, while unlikely, may not be the auctioneer himself. Also, the comparison is done against the to address specified, not the caller / msg.sender as the comment implies.

- Amount paid to whomever started the auction. 0 if it's the same address that's calling this method
+ Amount paid to address specified by whomever started the auction. 0 if it's the same as the `to` address

L04: Incorrect natspec for setLimit()

Line References

https://github.com/code-423n4/2022-07-yield/blob/6ab092b8c10e4dabb470918ae15c6451c861655f/contracts/Witch.sol#L118-L122

Description

The comments seem outdated as the only parameter that is updated by the function is the maximum collateral that can be concurrently auctioned off.

///  - the auction duration to calculate liquidation prices
///  - the proportion of the collateral that will be sold at auction start
///  - the maximum collateral that can be auctioned at the same time
///  - the minimum collateral that must be left when buying, unless buying all
///  - The decimals for maximum and minimum

Suggest removing / updating the referenced comments.

Non-Critical Findings

NC01: Modify comment to soft limit check for clarity

Line References

https://github.com/code-423n4/2022-07-yield/blob/6ab092b8c10e4dabb470918ae15c6451c861655f/contracts/Witch.sol#L194-L196

https://github.com/code-423n4/2022-07-yield/blob/6ab092b8c10e4dabb470918ae15c6451c861655f/contracts/Witch.sol#L200

https://github.com/code-423n4/2022-07-yield/blob/6ab092b8c10e4dabb470918ae15c6451c861655f/contracts/Witch.sol#L204

Description

The limit check is done before the summation to the total collateral allowable for liquidation. One may consider this to be a bug, but the README explains why this is the case:

Note that the first auction to reach the limit is allowed to pass it, so that there is never the situation where a vault would be too big to ever be auctioned.

The inline comments have this as well, but isnโ€™t as clearly put as the README.

// There is a limit on how much collateral can be concurrently put at auction, but it is a soft limit.
// If the limit has been surpassed, no more vaults of that collateral can be put for auction.
// This avoids the scenario where some vaults might be too large to be auctioned.

For greater clarity, I would suggesting modifying the inline comment to be worded similar as the README.

// There is a limit on how much collateral can be concurrently put at auction, but it is a soft limit.
- // If the limit has been surpassed, no more vaults of that collateral can be put for auction.
+ // The first auction to reach or exceed the limit is allowed to pass it, but subsequently, no more vaults of that collateral can be put for auction.
// This avoids the scenario where some vaults might be too large to be auctioned.

NC02: Typos

- bellow
+ below

- differente
+ different

// Extra spacing
- The Join  then dishes out
+ The Join then dishes out

- quoutes hoy much ink
+ quotes how much ink

#0 - alcueca

2022-07-22T14:00:41Z

This is a great QA report :heart:

#1 - PierrickGT

2022-08-12T16:48:54Z

Best QA report of this contest with clear description of the problems and comprehensive suggestions provided.

L01: Vaults that are over-collateralised after partial liquidation are possibly subject to further liquidations

Great suggestion and explanation of the risks induced by the smart contract architecture and design of the Witch contract. As mentioned by the warden, his suggestion may complexify the code but he did a good job of outlining the risks and the sponsor can now decide to implement or not his suggestion.

L02: Comparison in _calcPayout() should include equality

Great find and recommandation that will cover the edge case when elapsed = duration = 0.

L03: Incorrect description for auctioneerCut

The comment was indeed not entirely clear and the suggestion provided by the warden should avoid any confusion in the future.

L04: Incorrect natspec for setLimit()

Good recommandation, it's always best to write up to date Natspect documentations to avoid any confusion during a future refactor of the code.

NC01: Modify comment to soft limit check for clarity

Not critical but the suggestion from the warden adds a bit of clarity to the comment.

NC02: Typos

It's always best to avoid typos in the documentation of the code.

G01: Redundant casting

Line References

https://github.com/code-423n4/2022-07-yield/blob/6ab092b8c10e4dabb470918ae15c6451c861655f/contracts/Witch.sol#L444

Description

The casting of artIn to u128 is redundant in auction_.art -= artIn.u128(); because the subtraction auction_.art - artIn would have reverted when checking dust a couple of lines prior.

- auction_.art -= artIn.u128();
+ // casting to u128 is redundant because it would have reverted in L438
+ auction_.art -= artIn;

G02: Unchecked subtraction

Line References

https://github.com/code-423n4/2022-07-yield/blob/6ab092b8c10e4dabb470918ae15c6451c861655f/contracts/Witch.sol#L598

Description

liquidatorCut -= auctioneerCut; can be unchecked because auctioneerReward <= 1e18, so we know auctioneerCut is guaranteed to be <= liquidatorCut

unchecked { liquidatorCut -= auctioneerCut; }
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