Yield Witch v2 contest - antonttc'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: 2/63

Findings: 2

Award: $5,083.02

🌟 Selected for report: 1

πŸš€ Solo Findings: 0

Findings Information

🌟 Selected for report: antonttc

Also found by: 0x52

Labels

bug
3 (High Risk)
sponsor confirmed

Awards

5032.8947 USDC - $5,032.89

External Links

Lines of code

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

Vulnerability details

Impact

might lead to systematic debt. Cause errors for liquidators to run normally.

Proof of Concept

In the function auction, there is on input validation around whether the to is address(0) or not. and if the auctioneerReward is set to an value > 0 (as default), each liquidate call will call Join module to pay out to auctioneer with the following line:

if (auctioneerCut > 0) {
    ilkJoin.exit(auction_.auctioneer, auctioneerCut.u128());
}

This line will revert if auctioneer is set to address(0) on some tokens (revert on transferring to address(0) is a default behaviour of the OpenZeppelin template). So if someone start an auction with to = address(0), this auction becomes un-liquidatable.

A malicious user can run a bot to monitor his own vault, and if the got underwater and they don’t have enough collateral to top up, they can immediately start an auction on their own vault and set actioneer to 0 to avoid actually being liquidated, which breaks the design of the system.

Add check while starting an auction:

function auction(bytes12 vaultId, address to)
    external
    returns (DataTypes.Auction memory auction_)
{
    require (to != address(0), "invalid auctioneer");
		...
}		

#0 - HickupHH3

2022-07-18T14:58:48Z

dup of #46

#1 - alcueca

2022-07-21T10:22:23Z

Let's take this one as main.

#2 - alcueca

2022-07-21T10:22:56Z

Best finding of the contest :trophy:

#3 - PierrickGT

2022-08-03T16:11:11Z

Most critical vulnerability found during the audit since a malicious user could open a vault and never get liquidated, it would force the protocol to take on bad debts. The warden did a great job of describing the issue and providing the sponsor with a detailed fix. For the reasons above, I have ranked this issue as the highest one of this contest.

Gas Optimisation

1. cache proportion in _calcAuction can save gas from reading storage.

in function _calcAuction, only proportion of the line struct is used, we can use the following syntax to only read that 1 field from the struct and store it locally.

Original Code

// We store the proportion of the vault to auction, which is the whole vault if the debt would be below dust.
DataTypes.Line storage line = lines[vault.ilkId][series.baseId];
uint128 art = uint256(balances.art).wmul(line.proportion).u128();
if (art < debt.min * (10**debt.dec)) art = balances.art;
uint128 ink = (art == balances.art)
    ? balances.ink
    : uint256(balances.ink).wmul(line.proportion).u128();

After caching

uint64 proportion = (lines[vault.ilkId][series.baseId]).proportion;
uint128 art = uint256(balances.art).wmul(proportion).u128();
if (art < debt.min * (10**debt.dec)) art = balances.art;
uint128 ink = (art == balances.art)
    ? balances.ink
    : uint256(balances.ink).wmul(proportion).u128();

Gas saving: around 100 gas.

auction: 91148 β‡’ 91031

calcPayout: 19151 β‡’ 19034

2.Simplify proportionNow calculation in _calcPayout

in the branch (elapsed < duration) while calculating proportionNo, two small optimisation can be applied:

  1. unchecked block can be use since 1e18 - initialProportion cannot underflow
  2. don’t need to use WMul and wDiv on elapsed and duration, they are the same unit and the /WAD and *WAD cancel out

The function can be simplified as follow

if (duration == type(uint32).max) { // same
    proportionNow = initialProportion;
} else if (elapsed > duration) {
    proportionNow = 1e18; // same
} else {
    unchecked {
        proportionNow =
        uint256(initialProportion) +
        (uint256(1e18 - initialProportion) * elapsed / duration);
    }
}

Gas saving: 400 gas saved on _calcPayout .

  • calcPayout: 19151 β‡’ 18713
  • payBase: 26694 β‡’ 26256
  • payFYToken:27910 β‡’ 27472

3. Simplify inkAtEnd calculation in _calcPayout

Similar to the previous report: wMul and wDiv cancelled out and make it more expensive to do the additional *WAD and /WAD. we can save this without lost of precision.

Original code:

uint256 inkAtEnd = uint256(artIn).wdiv(auction_.art).wmul(auction_.ink);

Simplified code:

uint256 inkAtEnd = uint256(artIn) * auction_.ink / auction_.art;

Note: need to apply multiply before division to prevent lost of precision.

Gas savings: around 220 gas on _calcPayout

  • calcPayout: 19151 β‡’ 18945
  • payBase: 26694 β‡’ 26488
  • payFYToken:27910 β‡’ 27704

4. Avoid external call in payBase

In the current code, it calculates the real baseIn by calling cauldron.debtToBase and pass in artIn . the variable artIn is actually derived by parameter maxBaseIn, so converting it back and forth (base to debt to base) is unnecessary, if artIn is not changed by the following line:

// If offering too much base, take only the necessary.
artIn = artIn > auction_.art ? auction_.art : artIn;

A overal more efficient implementation is

if (artIn > auction_.art) {
      artIn = auction_.art;
      baseIn = cauldron.debtToBase(auction_.seriesId, artIn);
  } else {
      baseIn = maxBaseIn;
  }

So we can avoid calling debtToBase again if artIn is not higher than collateral in the vault, which should be most cases.

Gas savings: around 450 gas on payBase

  • payBase : 26694 β‡’ 26134

5. Simplify setLimit

The current code of setLimit involves unnecessary operation on reading the sum value and setting the same value back to storage. This can be changed to only update the max value.

Original

limits[ilkId][baseId] = DataTypes.Limits({
    max: max,
    sum: limits[ilkId][baseId].sum // sum is initialized at zero, and doesn't change when changing any ilk parameters
});

Simplified version. also increase readability.

limits[ilkId][baseId].max = max;

Gas saving:

  • setLimit: 27218 β‡’ 27100 (118 gas)

6. Enable Optimizer

the current config doesn’t enable optimizer, enabling it can save around 200~1200 gas, depends on functions.

update to foundry.toml

optimizer = true
optimizer_runs = 1000000

Gas savings: (most significant)

  • auction: 91148 β‡’ 89863 (around 1200 gas)
  • calcPayout : 19151 β‡’ 18269 (900 gas)
  • payBase: 26694 β‡’ 25296 (1400 gas)
  • payFYToken: 27910 β‡’ 26797 (1100 gas)
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