Date Addressed and Resolved: September 27, 2021

Underflow Error When Activating Cooldown

By: Evert Kors

Issue Raised: UI Not Allowing Cooldown Activation

User WOO posted on Discord at 5:13AM UTC that the UI was not allowing cooldowns to happen.

I was asleep during that time and woke up around 9:00AM UTC.

Jack pinged me between 5:55AM and 7:29AM UTC with various takes and ideas about what it could be.

Less than 12 hours ago we had transferred 28.5M USDC from the staking pool into AAVE to activate the yield strategy. This was, of course, a very significant event.

Investigation

I started by checking if any suspicious transactions had occurred over the last day to our core and strategy deployments — this wasn’t the case.

As our contracts had been thoroughly audited, my first guess was that there was something wrong with the UI itself. By logging the parameters used to initiate the cooldown transaction it was quickly clear this wasn’t the case.

I duplicated the transaction in a hardhat environment and executed it using a fork of mainnet —the transaction preliminarily failed as the gas could not be estimated (because the transaction would fail).

I tried doing the same transaction on our recently deployed Kovan testnet instance which yielded the same result.

I was still completely in the dark as to what was happening because we’d been testing this setup in production for months on end, and suddenly it breaks. It would turn out this deployment was significantly different from our previous ones.

Solution

This deployment didn’t yield any premium rewards yet. This meant 0 SHERX (Sherlock’s reward token) was being minted on everyone’s stake.

The reward calculation is done before the lockUSDC transfer to make sure every user receives the right amount of rewards for the period of time they hold onto the token. Activating the cooldown triggers a lockUSDC transfer to the core contract itself.

This piece of code, executed on lockUSDC transfer, checks if there are SHERX rewards available. As noted before, in this stage it should always be 0.

I was able to force a transaction on goerli by setting a custom gasLimit (and skipping the estimate gas process).

Failing transaction

At 11:21 AM, the Tenderly transaction debugger showed why it was reverting:

https://i.imgur.com/et2IpEz.png

It was basically doing a - b but b > a, causing an underflow and making the transaction revert because of SafeMath.

I assumed the revert happened because raw_amount (a) was too small as this variable is calculated using variables shared across users. And we already knew that this issue was hitting multiple users.

raw_amount is calculated using ps.sWeight. There are 3 places in the code where this variable is mutated:

  1. Minting lock tokens (adding value)
  2. Burning lock tokens (subtracting value)
  3. Adding SHERX rewards (adding value)

Options 2 and 3 hadn’t happened yet so only Option 1 could have caused the small difference. I asked myself, “Why did this only happen after we activated the AAVE strategy?” Before the strategy, the exchange rate of USDC and lockUSDC was 1:1. But as lockUSDC became worth more than 1 USDC (because of the accruing interest) the exchange rate started shifting.

Before:

10**6 USDC = 1000000000000000000 lockUSDC

After:

10**6 USDC = 999999999999999999 lockUSDC

The change in exchange rate caused a very small loss of precision, ultimately leading to the variables in the Tenderly screenshot.

Mitigation

Adding SHERX rewards (as shown in the list above) will make up for the loss of precision. This is because it adds value to the sWeight, which ensures the statements previously failing (due to the SafeMath error) will now result in a positive sum and accrue SHERX rewards.

This action was executed at 12:03PM UTC, enabling the cooldown again.