Legacy Smart Contract Vulnerability: Post Mortem Analysis

Corey Caplan
Dolomite
Published in
6 min readMar 29, 2024

Summary

On March 20, 2024, we were made aware of a vulnerability in Dolomite’s old product on Ethereum Mainnet that was deployed in 2019 and spun down in 2020. Users who had stale approvals on the old system were exposed to assets being stolen from their wallet if they maintained approvals on the Loopring Trade Delegate smart contract.

The Dolomite team immediately disabled the vulnerable smart contract and suspended the old system. Despite taking swift action and taking control over the situation in less than 1 hour, approximately 1,245,271 USDC, 94,423 DAI, and 165.9 WETH (totaling $1.8M) was taken from 187 victims.

By March 24, 2024 at 3:44:35 PM UTC, we recovered 90% of the assets that were taken by the exploiter. We then chose to use Dolomite’s treasury to make users whole for the remaining 10% of assets. We spent the proceeding days combing through the transactions and assembling a precise list of users whose assets were taken. On March 26, 2024, we sent the stolen funds and our contribution back to each victim in the following transactions:

Primer on Old Dolomite & Loopring

Back in the early days of DeFi, when it was still called Open Finance, Dolomite was a Loopring-based exchange that utilized an order book and offered off-chain order matching with on-chain settlement. The infrastructure was similar to what popular platforms like Vertex are today. Dolomite utilized dYdX’s Solo Margin system to enhance Loopring’s offering to enable margin trading.

To enable trading, users would approve tokens against the Loopring Trade Delegate, then sign typed messages that would compose an order. Each order’s validity was checked on the Ethereum blockchain after Dolomite’s Trade Execution Coordinator had batched matched orders for final settlement. When two orders were sent to the contract for settlement, they would be called a Ring (hence Loopring’s name).

Loopring used a very defensive style of coding/architecture that would attempt to continue processing a transaction under most circumstances. If a ring/order was considered invalid, rather than revert, it would emit an InvalidRing event instead. Nowadays we know this to be an antipattern in Solidity. Current best practices call for transaction execution to cease and revert as soon as any issue is encountered.

Loopring was extremely optimized for gas efficiency. Notably, the protocol used a custom byte compaction algorithm for calldata, skipped checks/validation that was deemed redundant, and used assembly to emit events & create data structures to squeeze every bit of performance out of the EVM.

Details of the Vulnerability

The attacker submitted a series of transactions that contained a number of invalid orders and 2 valid orders. The invalid orders were used to transfer the victims’ funds to DolomiteMarginExchange and create actions to be executed within SoloMargin. The two valid orders were used to exchange all of the victims’ supplied funds for a small amount of LRC.

Walking through the call stack, here is a break down of what happened:

  • The submitRingsThroughDyDx function is used to execute a number of rings within Loopring. Each ring can consist of two orders. The orders and rings go through validation checks within the Loopring code (more on this later). This function call assumes that the user wants to perform a margin trade. If a margin trade is being opened, collateral must be posted to the position, from the user’s wallet.
  • Based on the rings and order data that the transaction submitter provides, DolomiteMarginProtocol will craft the appropriate actions that will be executed within the operate call to SoloMargin. The call to ctx.requireTokenTransfer(…) is what caused the victim’s funds to be transferred to the DolomiteMarginProtocol contract. Without being used in a trade, the transaction would have reverted and these funds would have never left the users’ wallets. However, the attacker was able to execute a trade with these funds after they were withdrawn from each user’s wallet, spoofing the system into thinking these “margin deposits” were legitimate. More on this soon.
In _generateDydxPerformParams the Call action is created that will eventually invoke submitRings on the Loopring protocol. This function enqueues the malicious margin deposits via ctx.requireTokenTransfer(…). This function assumes the margin deposits are validated in the call to submitRings.
  • operate is called on SoloMargin and all of the actions are executed. The first few actions are a series of deposits and withdrawals where a margin deposit is executed on behalf of the victim. These margin deposits are what was stolen from users’ wallets.
  • After the margin deposits are executed, callFunction is invoked on DolomiteMarginProtocol, which is used to invoke submitRings on Loopring.
  • Loopring was responsible for doing order and ring validation. The two most important functions with regards to the exploit are RingSubmitter#batchGetFilledAndCheckCancelled, and OrderHelper#check.
  • batchGetFilledAndCheckCancelled calls tradeHistoryAddress to get the status of the orders. If the order is canceled or invalid, the Loopring TradeHistory contract sets order.filledAmountS to ~uint256(0). The filled amount is really important here, because it represents how much was previously filled for an order. Notably, Loopring’s defensive coding style opted to return a value that’s non-zero! Nowadays, the norm is to revert and terminate the transaction.
  • Within OrderHelper.check(…), if the order is partially filled (meaning order.filledAmountS > 0), then the order signature is not checked and instead only validateUnstableInfo is called. Thus, the signature verification check was skipped since the exploiter sent through invalid orders whose order.filledAmountS was not equal to 0 by coercing the invalid order’s state in the prior bulletpoint.
The check function in Loopring optimized for gas by skipping signature validation (line 176) if an order is partially filled. If the order’s signatures were checked for validity at this point in the call stack, the transaction would have reverted and the exploit would not have been possible.
  • Continuing through the RingSubmitter#submitRings call, eventually it loops through each ring provided and calls ring.checkOrdersValid(). This is only done once because the attacker only submitted one ring with two valid orders. The other orders were effectively ignored.
  • This lack of input validation on the ignored orders was also a performance optimization to save gas and continue transaction processing without reverting.
  • The one valid ring is executed which does the exchange of all the victim’s funds for a tiny amount of LRC.
  • In conclusion, the attacker was able to craft Actions to be executed within SoloMargin by using invalid orders. Loopring never reverted because the exploiter coerced Loopring’s smart contracts to skip signature validation, by setting order.filledAmountS = ~uint256(0), and the orders were never included within any rings, which prevented further validation from occurring. The DolomiteMarginProtocol smart contract assumed the underlying call to Loopring would perform the appropriate validity checks on Loopring’s orders.

Learnings

DeFi has certainly come a long way from its humble beginnings when those smart contracts were originally deployed. Thematically there are two key takeaways from the exploit:

  1. When you optimize too much for gas, you end up optimizing for nothing! Meaning, excessively optimizing for gas consumption can lead to critical exploits or strange behavior that enables the manipulation of state with the smart contracts.
  2. Defensive programming with composable smart contracts is an anti-pattern. Nowadays, smart contracts that build on top of other smart contracts expect invalid input or erroneous state to cause reversions, not bubble up errors or try to recover from them!

Fortunately, these are both concepts that Dolomite has left behind. As DeFi evolved, the best practices did too, and we’re happy that Dolomite has stayed in lockstep with them.

Thank you

We’d like to extend a huge thank-you to everyone involved in the recovery effort! Each volunteer swiftly joined the war room and worked closely with the team to aid in the investigation, negotiation, and swift recovery of the victims’ funds. We couldn’t have done it without you!!

Our advisors and collaborators in no specific order:

  • Alicia Katz
    Polygon
    X: @aliciakatz
  • Ogle
    Ogle Security
    X\Tg: @cryptogle
    https://oglesecurity.com
  • Igor Igamberdiev
    Head of Research at Wintermute
    X: @FrankResearcher
  • Pcaversaccio
    “Working on what’s next.”
    X: @pcaversaccio
  • Kong
    SlowMist
    Tg: @kon97yc43
  • Blue
    SlowMist (CTO)
    Tg: @blue0101
  • Julia Hardy
    Chainalysis
    X: @julia27eth
    Tg: @julia_hardy

--

--

Corey Caplan
Dolomite

Founder of Dolomite, software engineer, and DeFi enthusiast.