Technical Post Mortem — Umbrella Chain Exploit
On Sunday, May 8th, hackers managed to exploit a bug in Umbrella’s Chain oracle smart contract, manipulating the price feed data of two of our First Class Data pairs, MAHA-USD and FTS-USD. No Layer 2 Data was manipulated. The subsequently manipulated prices were used to further exploit the users of that data, MahaDAO and Fortress Loans, respectively. This post will look to explain the course of events that led to exploitative attacks by malicious third parties on our oracle.
First, the details on-chain:
- The transaction that exploited the oracle: Link
- The Malicious contract: Link
- BlockId:
409819
- BSC block number 17634663
Background: What Made The Attack Possible
There was a bug in the chain contract (and its related ancillaries), and it was not audited back in August of 2021 when it should have.
Most codebases keep evolving (almost daily) to add new features, improvements, and optimizations. Given our aggressive roadmap and limited resources at that time, there was a missed opportunity for peer review and code audit.
Pre-August 6th, our chain contract would check balances of validator wallets that submitted signatures (basically submitted blocks to get added to the blockchain network). A 2/3 of consensus was required before a block of transactions (root hash and FCDs) could be added to the blockchain network.
To ensure that community validators do not collude and abuse the consensus requirements, we implemented a PoA (Proof of Authority) consensus mechanism while working on rolling out our DPoS (Delegated Proof of Stake) consensus.
4 signatures would be needed before a block could be added on-chain, irrespective of power (volume of UMB tokens held by individual validators). The goal was simple — make sure that any entity could not buy a lot of tokens and force a malicious transaction to get added on-chain.
On August 6th the PR with the bug was created. The bug was that instead of counting valid signatures, the code would count the total number of signatures and give a green light if the total (of valid and invalid) signatures was 4 or more.
On August 10th, in this commit, the bug was introduced but not activated.
Then, it was deployed, and then another commit was deployed.
August 31st: Bug Gets Activated
On this day, we turned off the mandatory balance check because we thought that the PoA requirement of 4 signatures (this number could be increased for more security at will) would do the job adequately.
Deploying this code also activated the bug.
After that, three more contracts were deployed with the same bug after August 31st (the last one on Dec-13–2021).
How the Attacker Exploited the Bug
As per the padding set in our chain contract, an entity can only submit one transaction every 60 seconds. The attacker submitted a transaction, front-ran the real validator, and because the chain contract was now only counting total number of signatures (instead of valid signatures), the malicious transaction (with incorrect pricing data’s root hash and FCD) was committed on-chain.
Now, the incorrect FCD could be used until the next block was minted (with the correct data).
The attacker wrote a smart contract to do the above and could now abuse the partner platforms with the incorrect FCD.
Has The Bug Been Fixed?
Yes.
The fix was to count signatures that have positive balances only (non-verified validators have a balance of 0)and redeploy the contract.
Once redeployed, the whole protocol was updated automatically with the new contract address.
Execution Flow of Attack on Fortress Finance
- Fortress
Unitroller
(behind Proxy) was called - Fortress
GovernorAlpha
called,execute()
the method with proposal id11
, the one that the attacker created. This creation was not part of this tx, but the execution of this proposal was.
proposal details:
[ proposals method Response ]
id uint256 : 11
proposer address : 0x0dB3B68c482b04c49cD64728AD5D6d9a7B8E43e6
eta uint256 : 1652041513
startBlock uint256 : 17490884
endBlock uint256 : 17577284
forVotes uint256 : 415967882226291982976102
againstVotes uint256 : 0
canceled bool : false
executed bool : true
proposal actions:
[ getActions method Response ]
targets address[] : 0x67340Bd16ee5649A37015138B3393Eb5ad17c195
values uint256[] : 0
signatures string[] : _setCollateralFactor(address,uint256)
calldatas bytes[] : 0x000000000000000000000000854c266b06445794fa543b1d8f6137c35924c9eb00000000000000000000000000000000000000000000000009b6e64a8ec60000
input for _setCollateralFactor
: FTS tokens address and 700000000000000000
(7e17
).
The attacker started preparing proposals on 28 April.
He made several proposals but only one was used in this tx.
The attacker voted for the proposal on May 6 and then he added it to the queue.
From the queue, the attacker did the following:
- Fortress
Timelock
was called (this is part of the whole governor execution) submit()
was called onChain
contract and invalid block409819
was created- then lending protocol was abused by depositing and borrowing assets based on invalid FTS price
Execution Flow of Attack on MAHADAO
- MahaDao (via proxy) call
- MahaChildToken called via proxy delegation
- input:
0x095ea7b3000000000000000000000000d55555376f9a43229dc92abc856aa93fee617a9affffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
- this is
approve(
BorrowerOperations, unlimited)
- Another call to MahaChildToken
- input:
0x70a08231000000000000000000000000cd337b920678cf35143322ab31ab8977c3463a45
- this is
balanceOf(ExploiterContract)
- BorrowerOperations (
0xd55555376f9a43229dc92abc856aa93fee617a9a
) was called forgetPriceFeed
and returned the PriceFeed contract - BorrowerOperations called PriceFeed
fetchPrice
and returned2e+34
(2_000_000_000_000_000
in 18 decimals) UMBOracle
was called inside, this is Maha contract deployed on Feb 13, with UMB oracle implementation that pulls price for keyMAHA/USD
, at the time of writing, the reported price is2.14
in 18 decimals.- key:
0x0000000000000000000000000000000000000000000000004d4148412d555344
- FCD value set by the attacker:
40_000_000_000_000_000e18
, this invalid price was from block17634663
to block17634701
- the attacker did not send any data to the contract while creating tx, so this was probably included in the contract code, that executed tx
- ActivePool (Maha protocol) was called
getETH
to get info about the amount of deposited ETH (output was233207
ETH) - DefaultPool called for
getETH
output was14182
ETH - ActivePool.getLUSDDebt() called, output was
210129
- DefaultPool.getLUSDDebt() called, output was
18589
- TroveManager called via proxy
- .getTroveStatus(
0xcd337b920678cf35143322ab31ab8977c3463a45
) called with attacker address, output was0
. (it means not exists) - .decayBaseRateFromBorrowing() was called (Updates the baseRate state variable based on time elapsed since the last redemption or LUSD borrowing operation.)
- LiquityLUSDToken.mint() (ARTH Valuecoin (ARTH)) was called for Governance contract with value
5_000_000e18
- Governance contract was called, method
sendToFund()
- proxy calls + fallbacks TBD
- attacker contract (
0xcd337b920678cf35143322ab31ab8977c3463a45
) deposit to ArthUSDWrapper (it is a rebase token) - amount
1_000_000_000.000000000000000000
- Exchanged on a pool that was created 70 days ago
- Another token exchange occurred on a pool made 30 days ago