Allbridge Core exploit summary
On the night of April 1st to 2nd, Allbridge Core protocol was exploited in a series of attacks. The attackers drained approximately $650k from both BNB Chain liquidity pools before the bridge was shut down.
By working together with our partners, we were able to recover most of the stolen funds and used them to provide recovery payments to all of the affected users who submitted the application form. Simultaneously, we investigated the causes behind the attack and implemented new features to Allbridge Core to prevent this type of exploit from happening in the future.
In this article, we will analyze the underlying problems of the previous version of liquidity pools that led to the exploit and how we fixed them. Even though the article goes in-depth about some of the intricate details of the Allbridge Core’s inner logic, this article does not require you to have any technical knowledge. If any questions remain by the end of the article, our support team will be happy to assist you.
Each Allbridge Core liquidity pool is essentially a swap contract, similar to the existing decentralized exchanges like Uniswap or Curve Finance. What is different about our pools, though, is the fact that liquidity is provided in just one token (unlike the regular DEXes, which require two tokens).
When liquidity is deposited, we match the deposited amount by minting the same amount of vUSD tokens*. This token is used as an intermediary to transfer value from one pool to another (either within the same blockchain or between two different blockchains).
*vUSD token is an abstraction used within the Allbridge Core ecosystem. Technically it is not a token, as it never gets out to user balances as an actual token. However, it is easier to explain the concept of how Allbridge Core works using this “token” analogy.
For example, if you bridge BUSD from BNB Chain to USDT on Tron, first we swap BUSD to vUSD, then transfer the value of vUSD to Tron via the cross-chain messaging protocol, then swap vUSD to USDT on Tron. In this particular direction of a swap, the internal price of BUSD to vUSD gets lower. At the same time, USDT to vUSD is higher, encouraging other actors to bridge in the opposite direction with profit.
Withdrawing the liquidity
So far, we’ve covered depositing liquidity and swaps, but what about the withdrawals? Withdrawing is similar to depositing, but in this case, the same amount of vUSD token is burnt as we return it to the user.
It is easy to do when the pool is balanced (having the same amount of liquidity and vUSD). However, we have to swap the difference between tokens to offset the imbalance. So the more imbalanced the pool is, the fewer tokens the user will get when withdrawing the liquidity*.
*Our liquidity providers noticed this effect when USDC temporarily depegged in March 2023. Even though there were more USDC than vUSD in USDC pools, the users could withdraw fewer tokens than were deposited initially (until the pools were relatively balanced again).
At this point, we’ve covered all the basics, but before we can continue with the problem which caused the exploit, we need to go through some swap pool fundamentals.
Exploring the swap fundamentals
We will get a bit more technical about how token swaps usually operate. You may skip this part if you’re familiar with this topic.
When tokens are deposited, the pool needs to measure how much liquidity was added to the pool. Different pools use different formulas (or curves) to calculate the liquidity*, but they all need to do this. The user receives the so-called LP tokens in exchange for providing tokens to the pool. They are a measure of the user’s contribution to the liquidity pool (and are also proportional to the liquidity increase).
*For example, classic Uniswap V1 and V2 pools and many similar DEXes use the mathematical product of the amounts to calculate the liquidity. So, if amount X of the first token and amount Y of the second token is added to the pool, liquidity increases by X * Y. Allbridge Core uses a more complex formula, but the underlying principle is the same.
Continuing with the swap, when tokens are swapped, the pool receives one of the tokens from the user and needs to calculate how much of the second token it needs to send back. This is done using the same formula for liquidity calculation, but in reverse: the liquidity needs to stay the same after the swap is finished. This is why this measure of liquidity is called the invariant.
And lastly, the liquidity withdrawal. When liquidity is withdrawn, the pool needs to calculate the number of tokens the user should receive back by burning the LP tokens proportional to the share of the liquidity pool the user wants to withdraw.
Allbridge Core liquidity pools
There are certain notable differences when it comes to Allbridge Core liquidity pools. The main difference is that there is only one token in the pool; the second one is the virtual token (vUSD), which does not exist. The error in our logic was the assumption that we should mint and burn the same amount of vUSD on deposit and withdrawal. While it functioned without issues for the balanced pools, it is what led to the exploit when the pools were extremely out of balance.
Due to our old and flawed logic, it was possible to withdraw more liquidity than the amount of LP tokens being burnt. The withdrawal itself was still unprofitable: if you deposited $100k to the balanced pool and tried to withdraw it all from the imbalanced one, you would receive less. However, the liquidity drain would disbalance the pool even further and leave it vulnerable to the next step: swapping very small amounts of tokens for very large ones.
Which brings us to the attack itself.
How did the attacker exploit the pools?
On the night between April 1st to April 2nd, the attack began. We cover the attacker’s actions step-by-step:
- The attacker took a flash loan and deposited $5m to the BUSD pool and $2m to the USDT pool on BNB Chain.
- Then, the attacker swapped $500k from USDT to BUSD. This caused both of the pools to become slightly imbalanced.
- After this happened, the attacker withdrew most of the provided liquidity from the BUSD pool (around $4.8m). The funds were withdrawn at a loss. However, due to the error in the logic, the BUSD pool became extremely imbalanced.
- The attacker swapped $40k BUSD back to the USDT. Due to the artificial pool imbalance, this value became around $800k in vUSD.
- The attacker proceeded with withdrawing all liquidity from both pools (again at a loss, but it was more than well compensated by step 4) and returned all the flash loans.
All of the actions were taken in a single transaction. As a result, the pools became extremely imbalanced, and bridge users started to rebalance the pools by bridging from other chains. Before the bridge was shut down, a second similar attack was executed, resulting in a smaller loss of about $160K.
We turned off the Allbridge Core UI and the bridge’s backend, but it was not enough to stop future attacks (since the attackers interacted directly with the smart contracts on the same chain). Thus, we increased the fee amount on the bridge to 100%, making these types of exploits unfeasible.
The attack was finally over, but the road to recovery had just begun.
The aftermath of the attack
After the attack, there was a liquidity deficit in every pool on all of the chains. The attack directly affected the BNB Chain, while other pools were affected by users bridging funds from other networks to the BNB Chain to offset the artificial imbalance.
We were able to retrieve most of the stolen funds from the first attacker, reopened the pools the next day for withdrawing the liquidity, and started a recovery program for those who could not withdraw their liquidity or withdrew at a loss.
Our top priority in the first couple of weeks was dealing with the consequences and supporting liquidity providers. Meanwhile, our developers and security consultants were working on the fix to relaunch Allbridge Core.
Developing a fix
Changes to liquidity calculations
The first thing we did was fixing the liquidity calculation on deposits and withdrawals. We implemented several fundamental changes:
- Now virtual vUSD tokens are fully backed by the deposited stablecoins. For example, if you deposit $100k USDC to the pool, we divide it proportionally between the token balance and vUSD balance ($50k each if the pool is balanced).
- On withdrawal, we also take the proportional amount of actual stablecoins and virtual tokens from the amounts in the pool. And since vUSDs are fully backed by the deposited stablecoins we can convert them 1:1.
After those changes, the logic became much more streamlined, and extensive testing was conducted to make sure that liquidity providers never withdraw more liquidity than LP tokens are burnt (which was the main flaw of the logic initially).
However, there was also another minor change to incentivize the correct user behavior.
For example, when the pool is balanced, the amount of LP tokens minted for the LP is always the same as the amount of liquidity provided (when you deposit $100k USDT, you will receive 100k LP tokens). If the pool is imbalanced, you will receive less.
But the same logic applies to withdrawals: under the new logic, it is profitable to withdraw the liquidity from the imbalanced pool, as you get more stablecoins than LP tokens burnt. What we did to fix this is to cap the withdrawals at the amount of LP tokens burnt. So if you have 100k LP tokens, you are guaranteed to withdraw $100k from the pool, no more, no less.
Reserves and rebalancing
In addition to tracking pool tokens and stablecoins, we must also track the pool reserves. Let’s consider the following example.
There are two pools, USDT and USDC, with $100k liquidity each. After transferring 10k USDT to USDC, liquidity is still $100k. However, the pool balances (which we will call reserves) are $110k USDT and $90k USDC, respectively (in reality, they are slightly less, but we will ignore the slippage in this example to make it simpler to understand).
While the first pool allows LPs to withdraw all of the liquidity, the second one has a reserve deficit of $10k. The funds are still there, but reserves essentially migrate between blockchains during the regular bridging operations.
This will not be noticeable most of the time; reserves will be rebalanced by Allbridge Core users (who will also be able to receive the rebalancing incentives). However, in extreme circumstances, we need a way to do this automatically.
It brings us to the introduction of the Rebalancer Authority. This a special account that can use the bridge and pay no fees to the system or the liquidity providers. It is reserved for exceptional circumstances only and will allow us to move the reserves between blockchains in the case of an emergency.
One asset per chain
To prevent the possibility of flash loan attacks, we will deploy a single liquidity pool per blockchain. Therefore, it will not be possible to execute an exploit in a single transaction. We may potentially return to the concept of multiple assets in the future once more extensive audits of our code are completed. But for now, we’re staying on the safe side.
Another security measure that we implemented addresses the instances of extreme pool imbalance.
Following the new changes, it will be impossible to disbalance the pools beyond a certain threshold. The affected pools will still be semi-operational, it will be possible to swap back and rebalance them, but not the other way round.
Additionally, this will offer liquidity providers better protection from unknown attacks. Pools will automatically shut down in case one of the stablecoins on the bridge loses its peg.
Even though the smart contracts were shut down during the attack, some valuable time was lost until the operations went to a complete halt. We plan to improve our reaction time in case of unexpected issues by adding new authorities. They will be able to stop the bridge operations immediately at the first signs of abnormal behavior.
Our support team will be able to selectively stop deposits, withdrawals, and swaps for the individual pools with their special new accounts. These accounts will not be able to restart the activities or manage fees and, of course, will not have access to user funds (not even the contract owner has such authority).
You can view this new feature as an emergency stop button that can quickly suspend the bridging activity if needed.
Becoming Open Source
We believe that transparency is vital for any DeFi protocol. To identify the potential vulnerabilities in advance, not only did we verify all the contract sources on block explorers, but we also published a public repository documenting the Core’s EVM and Tron contracts and our tests.
Once the internal tests are concluded for Solana, we will also add it to the public repository. We invite independent white hat researchers to review our bridge contracts and share their findings to prevent potential exploits.
As we are closing in on the relaunch, we decided to take this opportunity to share some of the major changes that will be coming to Allbridge Core. We remain dedicated to providing the best user experience with improved security protocols. Keep an eye on our social channels to learn more about the relaunch date.