Resources

People often ask me "How did you learn how to hack?" The answer: by reading. This page is a collection of the blog posts and other articles that I have accumulated over the years of my journey. Enjoy!

High Risk Bug Disclosure: Across Bridge Double-Spend- 1415

iosiro - Jason MatthyserPosted 1 Year Ago
  • Across protocol allows users to bridge funds between various EVM chains very fast - faster than finality. There are a couple of main users. First, the relayer who has funds on all chains. Second, the data worker for slow relays who always has enough liquidity.
  • The relayers search for a given transaction to have occurred within the EVM smart contract. If it's profitable and they have the liquidity on the other chain, they will do a transfer for them. If it's going too slow (aka it's not profitable) then the user can increase the fee. If it's profitable but the relayer doesn't have the funds then they call fillRelay() and the dataworker will handle it.
  • There are two types of events being used: deposit and fill. The deposit is what the relayer does and the fill is what the dataworker does, after something has happened to the initial fill. Being able to tie a fill to a deposit is important to ensuring that double spends don't occur - both for the on chain and off chain infrastructure.
  • There needs to be some fairly complex logic for ensuring that two deposits are not made to the user. Onchain, to prevent this, a hash of the deposit is made in order to track it. Offchain, the function validateFillForDeposit Fill() filters all recent fills to find the proper deposit for it.
  • The goal is to trick either the dataworker or the relayer to process the event when it should not. Within the relayer code, the function getValidUnfilledAmountForDDeposit() obtains the previous fills for the deposit against depositsWithBlockNumbers(). Additionally, there is a function that handles updates() that were being made to the transfer.
  • The relayerFeePct field would be updated for a sped up deposit within the local object. Since the hash of the original object and the new object were different, it saw that as a valid fill! The tying together portion of the code has been broken.
  • To exploit this, the following steps need to be done:
    1. Perform a transfer from chain A to chain B.
    2. Trigger a slow relay manually. This is to A) get the transfer in a different state and B) get the relayers to stop looking at it.
    3. Update the relayerFeePct on the source chain. This will get the relayer to see the deposit to NOT see it as a slow relay anymore.
    4. Transfer from relayer and dataworker is made. The hash is different than the original on the chain for both TXs. So, we steal funds!
  • To fix the issue, the client side properties now checked to ensure that clients could see the deposits that had been filled or not, regardless of the state of relayerFeePct. Personally, I don't like the client side fix very much; I feel like doing something with the hash would make more sense. Unfortunately, there are times where hashing too many things is just as bad as hashing too few.