rcs logo

April 20, 2024 Attack Postmortem

Starting Apr-20-2024 12:37:23 PM +UTC, an unknown attacker exploited a user-supplied calldata vulnerability to drain all of RCS’s funds into two external accounts. The total value of the attack was roughly $32k USD.

Exploit details

The attacker executed 29 transactions - one high-value rollup transaction that called flash multiple times, followed by 28 transactions each of which called flash once. With each call to flash, the attacker supplied data that caused RCS to either (a) call ERC20 transfer or transferFrom on tokens that were either held by RCS or that users had approved to RCS, or (b) call ERC721 safeTransferFrom on Uniswap V3 positions that were either held by RCS or that users had approved to RCS. In the latter case, most of the Uniswap V3 positions in question were empty.

The vulnerability was introduced about 2.5 years ago in commit 2b5cbae0270105db928caad9ced23a344511a404. Join was an old (pre-Diamond) module similar to DSS GemJoin that both held deposited external token balances, and also provided the flash loan entry point which called a user specified function. It made sense that as long as it always collects what it lends at the end of the transaction, the loan was safe. However, we did not consider the case where the external call would be made directly to a token contract rather than a contract with a set of actions to perform during the loan, thereby giving an attacker permission to do virtually whatever they wanted with the GemJoin address as if it was an EOA for which they owned the keys. This vulnerability propagated to the current version, where all the core RCS functionality occurs at a single address.

Detection and Response

The exploit was discovered about six hours later using an urn analysis tool alerting that the one position with Uniswap V3 NFTs as collateral was underwater. The protocol still recorded them as present, but after the attacker called reduceLiquidity they were empty. bail failed because the protocol didn’t own the NFT, so we investigated and soon discovered that RCS had been completely drained. To avoid further losses, we set ceil to zero and removed the flash entrypoint. Additional empty Uniswap v3 NFTs outside the protocol were stolen, all from the same address.

Remediation

We will reimburse all losses suffered by users in the same token they held before the attack. This includes losses due to transferred collateral, and also tokens they held outside the protocol and stolen via outstanding approvals to RCS.

TF = transferFrom to hacker (none of these were from BANK)
STF = safeTransferFrom to hacker, or bank approve to hacker that was later followed by a safeTransferFrom
T = transfer to hacker
BANK = 0x598C6c1cd9459F882530FC9D7dA438CB74C6CB3b
DRAINER = attacker

TXHASH,DESCRIPTION,HEIST,APPROX_USD_VALUE,AFFECTED_USER
0x5d2a94785d95a740ec5f778e79ff014c880bcefec70d1a7c2440e611f84713d6,T,10375.584869 USDC,10370.20,BANK
0x5d2a94785d95a740ec5f778e79ff014c880bcefec70d1a7c2440e611f84713d6,T,2478.235540754361715555 ARB,2949.10,BANK
0x5d2a94785d95a740ec5f778e79ff014c880bcefec70d1a7c2440e611f84713d6,T,69.669705859926416088 LINK,1030.41,BANK
0x5d2a94785d95a740ec5f778e79ff014c880bcefec70d1a7c2440e611f84713d6,T,0.266618989782460718 wstETH,975.65,BANK
0x5d2a94785d95a740ec5f778e79ff014c880bcefec70d1a7c2440e611f84713d6,T,0.287173939050505854 WETH,903.32,BANK
0x5d2a94785d95a740ec5f778e79ff014c880bcefec70d1a7c2440e611f84713d6,T,300 USDC.e,299.84,BANK
0x5d2a94785d95a740ec5f778e79ff014c880bcefec70d1a7c2440e611f84713d6,TF,4048.895272 USDC,4046.79,0x512e07a093aaa20ba288392eadf03838c7a4e522
0x5d2a94785d95a740ec5f778e79ff014c880bcefec70d1a7c2440e611f84713d6,TF,1000 USDC,999.48,0x83eccb05386b2d10d05e1baea8ac89b5b7ea8290
0x5d2a94785d95a740ec5f778e79ff014c880bcefec70d1a7c2440e611f84713d6,TF,0.325501656325727541 wstETH,1191.13,0x7b782a4d552a8ceb3924005a786a1a358ba63f71
0x1017444241a2efdb009eabeca85b356d89186f055aa35523885d8b3a407fdff8,TF,1.818 RICO,4184,0x7b782a4d552a8ceb3924005a786a1a358ba63f71
0x525b7c483c5055e046261e965384047cd7e7b5f2d487fd99ed0e64f6e6cc2cc3,STF,NFT 1214918,8740,BANK
0x7d5ea1cec6e9ab6e96b9e66841063f4342714837368ccaca4b980cfc36193537,TF,100 ARB,122,0x08fb8805166bb9f009ec4c4ec837bcc6a084424c
0x18252da1ce4fb3a012046bea04ce4d6ac9ea30e6fd30f58c25fb5f5edfdfedae,TF,50 ARB,61,0x42dbf634c256acd17beddc1330488f1bea7b8bdf
0x02f65ed2db2a004db6043dc46592702ab1142d6867febeae4a30423140388121,TF,0.0025 WETH,7.94,0xe524d6514f5c6c91796d223c3310ce211bc749c0
0x346918085e3945efa7195887b7d53f48c68729121a029d31d67c379ff3c90261,TF,0.0019101 WETH,6.07,0xc64844d9b3db280a6e46c1431e2229cd62dd2d69
0xcf24cfede8907b00cc307267c3013d1d0e945b4fc239d034076b201e02b2d97a,TF,1 USDC,1,0x6c5db2b7e8e664f93b00c5968e335999d294abab
0x5863303556fff020c6e38e6e6c8a1829ddfbc0a7d9dce4db690f8ca6071518ba,TF,113.799999999999737856 ARB,138.84,0x0d8761daad9aa860f33fdaf77150c99d0d2e1e25
0xd859dde6e575ede450cead2e3e60225facd4586e352cafe7d3b2728acc18154b,TF,4.06667 USDC,4.06,0x0d8761daad9aa860f33fdaf77150c99d0d2e1e25
0x61d48ff7c6d0835503db066bb440f08207ec322eac7d080d34a7f29741cf9fd2,TF,200 ARB,242,0xd09222b1486d8d1307ce9cd4c4f64749a2de23c1
0xcbc56e6b2444345f61c8b9f7cd4a07e58277c1e3eaf5a62983b0cf7fd578d0fa,TF,0.01838 WETH,58.4,0x7b782a4d552a8ceb3924005a786a1a358ba63f71
0x4970fcdcf92a23144ca40ad5745978c4eaaca243002ab43d0ef2b21e7bb69739,TF,0.0064688 WETH,20.55,0xc74a73576f9ca7c88c905edcc5f0f5f339d52380
0x826d51aad3759497dbdab49f2822f4e4214bc354fbfca81ba2d23aaf9aa77d52,STF,1097558,0,0x7b782a4d552a8ceb3924005a786a1a358ba63f71
0xebb6f3cb5d84c2d4b219a08a8acae62645b60c587549f6c1cb193fe997c95e52,STF,1099134,0,0x7b782a4d552a8ceb3924005a786a1a358ba63f71
0x3023cb454e14038126dc00084cb8b00786566092db3687d1040a70bd43593943,STF,1099230,0,0x7b782a4d552a8ceb3924005a786a1a358ba63f71
0x705ea6305cd0ec248d224915350fe3c77cce5775b9cd68cc74fe01894a85d9a0,STF,1099243,0,0x7b782a4d552a8ceb3924005a786a1a358ba63f71
0xa01540bdf8b09c4ca849e16d692d353bfb7ee25bc864ea12e794437fdc2c401d,STF,1099460,0,0x7b782a4d552a8ceb3924005a786a1a358ba63f71
0x94bb18b221faa600bf0ad590a0bde9c73c62803c7aba55895ed1cfa236a83560,STF,1101829,0,0x7b782a4d552a8ceb3924005a786a1a358ba63f71
0xaf50876e10cd14777832e0fe69bc908054254a981a590b418c88ca021ddcada2,STF,1102841,0,0x7b782a4d552a8ceb3924005a786a1a358ba63f71
0xb117ffb2ddf062a01a7bc0166a89fb211ff1b461e61a06e7c7410310fdf67e3e,STF,1102954,0,0x7b782a4d552a8ceb3924005a786a1a358ba63f71
0x6f5eae99330949f23c76479c0ad4b23036d577e7ed34a0db8449c9cab229242b,STF,1102961,0,0x7b782a4d552a8ceb3924005a786a1a358ba63f71
0xd959da407292dcbd445de51d218178b70f1153df8730bbe2fc205d282abbddf4,STF,1103133,0,0x7b782a4d552a8ceb3924005a786a1a358ba63f71
0xfdcbf81b637a55f38a6e898fec2919ebd6326083eac4f6f5c1c3e0efecfd7a05,STF,1105986,0,0x7b782a4d552a8ceb3924005a786a1a358ba63f71
0x2aa80807fae54908a8d1ad47b099ca7c405ca1e7336a2fe39011f959ba12de5b,STF,1106766,0,0x7b782a4d552a8ceb3924005a786a1a358ba63f71
0x9013b5f9872f6ccf0f445460b9b36badc5b8b13a0ef8a3e0d0ccdcd48a9c2478,STF,1108153,0,0x7b782a4d552a8ceb3924005a786a1a358ba63f71
0x09067dbbbaae6f8b1e5655355f04d75b0ae5df60fe00ee2bcaed76c41f2e8b30,STF,1108159,0,0x7b782a4d552a8ceb3924005a786a1a358ba63f71
0x461f40586ab761fd853a7cdb01dab0c7bcb888ecdfd14782e0011ebbe84febfe,STF,1130090,0,0x7b782a4d552a8ceb3924005a786a1a358ba63f71
0xea9b851dc6ff68a326d6a4bb73f79c49d70f29e52a2d6079071d4a81bb68d9c5,STF,1130132,0,0x7b782a4d552a8ceb3924005a786a1a358ba63f71

We will reimburse the last three transfers in the first transaction. For the rest, we will reimburse ink from the previous block.

The one RICO transfer will not be reimbursed, as RICO is uncollateralized and effectively worthless now.

All STFs except the first were of Uniswap V3 positions with zero liquidity and will not be reimbursed. The first contained 8,740.801338 USDC and some RICO - the USDC will be reimbursed.

The following transfers will be made to reimburse the transfers of assets that were held by non-BANK addresses:

TOKEN,AMOUNT,DESTINATION_ADDRESS
USDC,4048.895272,0x512e07a093aaa20ba288392eadf03838c7a4e522
USDC,1000,0x83eccb05386b2d10d05e1baea8ac89b5b7ea8290
wstETH,0.325501656325727541,0x7b782a4d552a8ceb3924005a786a1a358ba63f71
ARB,100,0x08fb8805166bb9f009ec4c4ec837bcc6a084424c
ARB,50,0x42dbf634c256acd17beddc1330488f1bea7b8bdf
WETH,0.0025,0xe524d6514f5c6c91796d223c3310ce211bc749c0
WETH,0.0019101,0xc64844d9b3db280a6e46c1431e2229cd62dd2d69
USDC,1,0x6c5db2b7e8e664f93b00c5968e335999d294abab
ARB,113.799999999999737856,0x0d8761daad9aa860f33fdaf77150c99d0d2e1e25
USDC,4.06667,0x0d8761daad9aa860f33fdaf77150c99d0d2e1e25
ARB,200,0xd09222b1486d8d1307ce9cd4c4f64749a2de23c1
WETH,0.01838,0x7b782a4d552a8ceb3924005a786a1a358ba63f71
WETH,0.0064688,0xc74a73576f9ca7c88c905edcc5f0f5f339d52380

The following transfers will be made to reimburse ink. We calculated these values using the printStats method in auction-keeper with fork block number set to 202973712. The last one (USDC 8740) reimburses the contents of a UniswapV3 NFT.

TOKEN,AMOUNT,DESTINATION_ADDRESS
WETH,0.06,0xc7c474d797d5edd63cef1b98e6155cf23fd36317
WETH,0.064544066495677313,0xc64844d9b3db280a6e46c1431e2229cd62dd2d69
WETH,0.1,0xc74a73576f9ca7c88c905edcc5f0f5f339d52380 
ARB,100,0xd09222b1486d8d1307ce9cd4c4f64749a2de23c1
ARB,2200,0x42dbf634c256acd17beddc1330488f1bea7b8bdf
ARB,136.097770833361325689,0x32a59b87352e980dd6ab1baf462696d28e63525d
wstETH,0.266618989782460718,0x7b782a4d552a8ceb3924005a786a1a358ba63f71
USDC,200,0xca9ba74ee20917211ef646ac51accc287f27538b
USDC,130.755365,0x9e255e1544e5d9b5c7766076890b059a27cafcc1
USDC,10000,0x512e07a093aaa20ba288392eadf03838c7a4e522
USDC.e,300,0x7c6accd51cbbdd53354de581841803b4f79d48e7
LINK,69.669705859926416088,0xe546d3813a63c70afc3af0c38a817581a0223c78
USDC,8740.801338,0x512e07a093aaa20ba288392eadf03838c7a4e522

Retrospective

A flash function might have made sense in DSS vat because there was a separate GemJoin module at a different address. This was a case of mapping refactorings that would probably have worked in an earlier system onto the forked system without keeping track of new permissions. DSS vat didn’t custody tokens. RCS vat did.

We might have caught the vulnerability during our review of differences from DSS had we not merged join and most other modules into vat, vow, and vox, and then later BankDiamond. It would have been more clear that Dai had an external contract dss-flash, and that its obvious analogue to join, GemJoin, did not have a flash loan function. As we refactored this connection became easier to miss.

It’s important when refactoring to make a list of major functionalities and how they’re affected by the refactoring. A simple analysis of the differences between join and GemJoin that highlighted flash being moved from dss-flash to join or GemJoin would have likely caught this early on. A later analysis of the relative locations of different functions versus the earlier version might also have caught it. Finally, it’s important to list the permissions of each module, especially when introducing external calls with user-supplied calldata.

Contract risk could have been reduced by avoiding a custom flash function and instead using a battle-tested ERC-3156 implementation. Even if it was still in the same contract as the one holding token balances the exploit would not have been possible as it calls onFlashLoan instead of a user specified low level call. A lesson here is that unnecessary flexibility should always be restricted to reduce the space of potential vulnerabilities. In this case, we used a low-level general call when an explicit call would have been more appropriate.

Our testing mainly focused on the minting and payback of tokens during the loan rather than the totality of things that can occur during the external call. The success of flash loans in other protocols likely lulled us into a false sense of security.

On a higher level, flash loans simply aren’t necessary. They’re an interesting feature that’s less possible elsewhere, but they don’t make for a substantially different product. No one in finance outside of blockchain is salivating at the idea of borrowing an infinite amount of money and paying it all back in one transaction, and flash loans will never make a so-so product a great one. We considered removing flash several times during RCS0’s lifecycle but decided it would be useful for auction keepers. In reality, core must always come first when writing something new, long before interesting features that are fun to build and that might make users’ lives easier.

The error which lead to the exploit was introduced before team review processes used during later stages of development had been established. This should have been identified as a risk. Preexisting code should go through the same degree of adversarial thought and testing as newer features. Everyone involved in testing should treat every line of code as if they wrote it themselves.

When the target network was changed to a rollup there was an opportunity to modify our design philosophy to prioritize security further above efficiency. Onchain invariant enforcement as a second layer of security may have prevented this, but was excluded considering the gas tradeoff in a potential Ethereum mainnet deployment. For example, for the ERC20 collateral, an invariant which required the external tokens balance to match the sum of internal balances would have prevented most of the heist.

Infinite approvals should be avoided in UIs. If our UI only approved what was necessary, the attack size would have been half of what it was.

Comments from Auditor