Platform: Code4rena
Start Date: 16/01/2024
Pot Size: $80,000 USDC
Total HM: 37
Participants: 178
Period: 14 days
Judge: Picodes
Total Solo HM: 4
Id: 320
League: ETH
Rank: 39/178
Findings: 1
Award: $372.80
π Selected for report: 0
π Solo Findings: 0
π Selected for report: israeladelaja
Also found by: PENGUN, VAD37, jasonxiale
372.7994 USDC - $372.80
https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/price_feed/CoreChainlinkFeed.sol#L44-L49 https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/stable/CollateralAndLiquidity.sol#L221-L236
Chainlink offchain oracle refresh price exactly every 60 minutes. And CoreChainlinkFeed
ignore chainlink feed if its price stale more than 60 minutes.
Looking at etherscan transactions, It is very easy to find chainlink feed price submission is delayed by a few blocks. Causing price updating longer than 60 minutes by 10-20 seconds a few times per day.
So each day on some specific blocks, chainlink price is ignored. The protocol expect chainlink and uniswap oracle to be available at all time.
The oracle price always average 2 out of 3 different and closest oracle price. Which most of the time are chainlink and uniswap TWAP.
With chainlink out of the picture, the fallback safety are CoreSaltyFeed
oracle and 7% price different between oracle protection.
CoreSaltyFeed
work similar to uniswapV2 reserve price. It can be manipulated if have enough token to swap.
This open up a single block window to manipulate oracle price by maximum 3.5%. When chainlink submit price late by 1 block, the oracle manipulated to return wrong price. Then next block with chainlink back online, oracle now return correct price. Enable arbitrage oracle profit.
Oracle price feed is used for stable coin USDS collateral. Attacker can borrow more USDS with manipulated oracle in one block then sell it back in next block for possible 3.5% profit.
Here is a sample contract of a chainlink aggregator update price to oracle feed. https://etherscan.io/address/0xE62B71cf983019BFf55bC83B48601ce8419650CC
It update price every 60 minutes exactly, when there is no big price movement, otherwise it update price immediately for every 0.5% price change. Most of the time, the timestamp aggregate between 2 rounds is exactly 60 minutes. And then there is a few times, the timestamp different is more than 60 minutes.
Look at timestamp of these 2 transactions from above contract: https://etherscan.io/tx/0xc39df382dc6d8b137f92f15f8a10d777c9eed960ce885bc5333d936d6e39da86 https://etherscan.io/tx/0xbdf4ac46502a12f18a75aa279e469a1d159e792b909b823203e2df28bae3d9ed
Both transmit to refresh price to oracle, it show the block.timestamp
different is more than 60 minutes.
03:55:47 AM +UTC vs 02:55:35 AM +UTC
.
And this is normal for price refresh, chainlink refresh price on-chain every hour.
So by frontrun chainlink oracle price submission on slow block every hour, CoreChainlinkFeed
will return 0 price because answerDelay > 60 minutes
.
PriceAggregator
will relied average price of CoreUniswapFeed
and CoreSaltyFeed
price feed.
For CoreSaltyFeed
, price can be manipulated.
Its mechanism work similar to uniswapV2 price reserve. Details is omitted here.
Just by having enough token to swap, price can drop in favor of attacker.
If CoreSaltyFeed
drop by 7% in comparision with uniswap TWAP, PriceAggregator
still accept feed as reasonable.
So now we have WBTC/USDS price drop 3.5% in single block transaction. This allow user more borrow power. Borrow with unfair price.
https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/stable/CollateralAndLiquidity.sol#L99
https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/stable/CollateralAndLiquidity.sol#L275
https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/stable/CollateralAndLiquidity.sol#L221-L236
Hopefully next block, chainlink will submit new price and chainlink timestamp it reset. With CoreChainlinkFeed
back online. PriceAggregator will use Chainlink and uniswap TWAP as the best price.
Attacker now just have to repay and profit from oracle manipulation.
Manual
max answer delay should be 61-70 minutes instead of 60 minutes.
Oracle
#0 - c4-judge
2024-02-02T16:16:59Z
Picodes marked the issue as primary issue
#1 - c4-sponsor
2024-02-08T11:50:43Z
othernet-global marked the issue as disagree with severity
#2 - c4-sponsor
2024-02-08T11:50:48Z
othernet-global (sponsor) confirmed
#3 - othernet-global
2024-02-17T22:54:15Z
Chainlink timeout now set to 65 minutes.
https://github.com/othernet-global/salty-io/commit/f9a830c61e77a22722a8e674a8affabe2a0cf04a
#4 - c4-judge
2024-02-19T11:43:41Z
Picodes changed the severity to 2 (Med Risk)
#5 - c4-judge
2024-02-19T11:44:00Z
Picodes marked issue #486 as primary and marked this issue as a duplicate of 486
#6 - c4-judge
2024-02-21T16:55:50Z
Picodes marked the issue as satisfactory