Contents

Twindex — Full Incident Analysis of Flash Loan and Price Oracle Manipulation Attacks


Originally published in Valix Consulting’s Medium.

This report analyzes the incident of Twindex’s fractional-algorithmic synthetic asset system exploited on October 2, 2021. We provide a full analysis explaining all attack details. Moreover, we also give remediation solutions to address issues in this report.

Table of Contents


.    .    .

TLDR


Twindex’s fractional-algorithmic synthetic asset system was attacked on October 2, 2021. The attacker executed two attack transactions, using the flash loan and price oracle manipulation attacks, to gain a profit of approximately $538,110.

Each attack transaction comprised of six steps, including:

  1. Borrowing Flash Loans From Twindex Pools

  2. Minting Fractional Synthetic Asset tXAU

  3. Draining the USDC Reserve of Dopple Tri Pool

  4. Manipulating the KUSD Price Oracle

  5. Redeeming the Minted Fractional Synthetic Asset tXAU

  6. Paying Back the Loans and Taking the Profit


.    .    .

How Does the Twindex’s Fractional-Algorithmic Synthetic Asset Work?


Twindex’s fractional-algorithmic synthetic asset (TSA for short) is a token mintable from a combination of the stablecoin KUSD and the Twindex platform token TWX. TSA uses both on-chain and off-chain price oracle to keep its price on the peg with the real-world asset price.

Figure 1. Example of Twindex’s fractional-algorithmic synthetic asset token with a collateral ratio of 69% (Image courtesy of the Dopple Finance)

Figure 1. Example of Twindex’s fractional-algorithmic synthetic asset token with a collateral ratio of 69% (Image courtesy of the Dopple Finance)


Figure 1 shows an example of the TSA token with a collateral ratio of 69%. Whereas the collateral ratio is adjustable dynamically depending on the market price, the TSA minting and redemption processes rely on this collateral ratio. Refer to this link for more details.

The following is an excerpted description of the TSA minting process.

Minting is the process of creating new TSA tokens by supplying KUSD and TWX according to the Collateral Ratio. The value of assets needed to be minted will be worth the market price at the time of minting. The collateralized KUSD will be used to provide value, while TWX will be burned from the system.

And, the following is an excerpted description of the TSA redemption process.

Redemption is the process of retrieving KUSD and TWX by supplying the TSA to burn. The ratio of KUSD and TWX depends on the Collateral Ratio but will be worth the market price.


Some popular TSA tokens include tXAU, tTSLA, tAAPL, tJPY, and tEUR.


.    .    .

Associated Addresses



.    .    .

Associated Transactions


There were two attack transactions in this incident as follows.


The attacker profited approximately $538,110 from this incident.


.    .    .

Full Incident Analysis


This section analyzes the incident in detail based on the first attack transaction, 0x24180e…a8d74a. The second attack transaction is identical in attack details, though.

The attack transaction can be dissected into six steps as follows.

Step 1: Borrowing Flash Loans From Twindex Pools


Figure 2. Borrowing flash loans from Twindex pools

Figure 2. Borrowing flash loans from Twindex pools


The attacker initiated the attack transaction by invoking Attacker Contract #1, as illustrated in Step 1 in Figure 2. Then, Attacker Contract #1 performed Steps 2–5 to borrow flash loans from Twindex pools, including:

  • Flash loan #1: borrowing 1,150,000 TWX (Step 2.1) and 290,000 KUSD (Step 2.2) from TWX-KUSD pool

  • Flash loan #2: borrowing 305,000 KUSD from DOPX-KUSD pool (Step 3)

  • Flash loan #3: borrowing 185,000 KUSD from tXAU-KUSD pool (Step 4)

  • Flash loan #4: borrowing 141,000 KUSD from tJPY-KUSD pool (Step 5)


Figure 3. Tokens transferred while borrowing flash loans

Figure 3. Tokens transferred while borrowing flash loans


Figure 3 shows the evidence of tokens transferred during the flash loans by Attacker Contract #1. Note that flash loan #1 was borrowing both the reserve assets of the TWX-KUSD pool (Step 1 and Step 2 in Figure 3).

In summary, Attacker Contract #1 borrowed 1,150,000 TWX and 921,000 KUSD by the flash loans in this attack step.


Step 2: Minting Fractional Synthetic Asset tXAU


Figure 4. Minting fractional synthetic asset tXAU

Figure 4. Minting fractional synthetic asset tXAU


After setting up the attack by borrowing the flash loans, Attacker Contract #1 supplied some of the borrowed TWX and KUSD tokens to mint the Twindex’s fractional synthetic assettXAU, as portrayed in Figure 4. This attack step can be briefly described as follows.

  1. Attacker Contract #1 called the mintFractionalSynth function of the SyntheticPool contract by supplying the borrowed 1,124,013.407442492934418687 TWX and 921,000 KUSD to mint tXAU tokens.

  2. SyntheticPool contract contacted the StablePoolOracle contract to read the KUSD (collateral) price from the KUSD-TWAP (Time-Weighted Average Price) parameter.

  3. StablePoolOracle contract returned the current KUSD price at that moment, $0.996647861433978465, to the SyntheticPool contract.

  4. SyntheticPool contract minted 606.195630970930049706 tXAU for the Attacker Contract #1. In this step, the supplied (from Step 1) 1,124,013.407442492934418687 TWX was burned, while 921,000 KUSD was locked into Twindex’s CollateralReserve contract.


Figure 5. The mintFractionalSynth function of the SyntheticPool contract

Figure 5. The mintFractionalSynth function of the SyntheticPool contract


Figure 5 shows the mintFractionalSynth function of the SyntheticPool contract. Attacker Contract #1 inputted the function arguments as follows.

  • Actual KUSD amount to lock as collateral: 921,000 KUSD

  • Maximum TWX amount approved to supply: 1,150,000 TWX


The following gathers the highlighted states at the minting time.

  • TWX price: $0.135709613434781496

  • KUSD price: $0.996647861433978465

  • tXAU price: $1,760.555

  • Collateral ratio: 85.75%


The mintFractionalSynth function did some calculations (line no’s. 188–209). And, the following highlights some computation results.

  • Actual TWX amount to burn: 1,124,013.407442492934418687 TWX

  • tXAU amount to mint: 606.195630970930049706 tXAU

  • Minting fee: 1.824059070123159629 tXAU


After the calculation process, the mintFractionalSynth function burned 1,124,013.407442492934418687 TWX from Attacker Contract #1 (line no. 213). The function then transferred and locked 921,000 KUSD as collateral from Attacker Contract #1 to the CollateralReserve contract (line no’s. 214–218). Next, the function minted 606.195630970930049706 tXAU for Attacker Contract #1 (line no. 219). Finally, the function minted 1.824059070123159629 tXAU as the minting fee (line no. 220).

Noteworthy, the SyntheticPool contract used the state variable lastAction to keep track of every minting requestor (i.e., msg.sender) and minting block number every time each tXAU token is minted in line no. 211. In this attack transaction, Attacker Contract #1 minted the tXAU token at block number 11406815.

The lastAction variable was used in the SyntheticPool contract to prevent flash loan and reentrancy attacks. From our analysis, however, the use of the lastAction variable turned out to be one of the bugs we found in the contract that made this attack incident possible. We will explain more about this bug in attack step 5.

Figure 6. Tokens transferred while minting the tXAU

Figure 6. Tokens transferred while minting the tXAU


The evidence of tokens transferred during the tXAU minting process is shown in Figure 6 above.

In summary, Attacker Contract #1 supplied the borrowed 1,124,013.407442492934418687 TWX and 921,000 KUSD to mint 606.195630970930049706 tXAU in this attack step.


Step 3: Draining the USDC Reserve of Dopple Tri Pool


Figure 7. Draining the USDC reserve of Dopple Tri Pool

Figure 7. Draining the USDC reserve of Dopple Tri Pool


Next step Attacker Contract #1 began the attack by borrowing the final flash loan #5 of 180,000 BUSD from the BSC-USD — BUSD LatteSwap pool (Step 1 in Figure 7). After that, Attacker Contract #1 swapped the borrowed 180,000 BUSD (Step 2) for 157,417.177469010566607885 USDC (Step 3) via KUSD-BUSD-USDC Dopple Tri Pool.

Since the swapped-in amount is large enough to almost empty the USDC reserve in the pool (Step 4), you can see that Attacker Contract #1 got a high price impact here. The percentage change between the swapped-in and swapped-out amounts is by far a ≈12.546% decrease.

Figure 8. The swap function of the LatteSwap contract

Figure 8. The swap function of the LatteSwap contract


Figure 8 shows the swap function of the LatteSwap contract. LatteSwap pool provided flash loan #5 of 180,000 BUSD to the Attacker Contract #1 (Step 1 in Figure 7) in line no. 384. Then, the function executed line no. 385 to trigger the callback function and redirect the control flow back to the Attacker Contract #1.

Figure 9. The swap function of the Dopple Swap contract

Figure 9. The swap function of the Dopple Swap contract


Figure 9 displays the swap function of the Dopple Swap contract. This function swapped the borrowed 180,000 BUSD for 157,417.177469010566607885 USDC via KUSD-BUSD-USDC Dopple Tri Pool (Steps 2 and 3 in Figure 7).

Figure 10. Tokens transferred while draining the USDC reserve

Figure 10. Tokens transferred while draining the USDC reserve


The evidence of tokens transferred during the USDC reserve draining process is presented in Figure 10.

In summary, Attacker Contract #1 triggered this attack step to drain almost all the USDC reserve of the KUSD-BUSD-USDC Dopple Tri Pool. This step is only the first phase of the KUSD price oracle attack.


Step 4: Manipulating the KUSD Price Oracle


Figure 11. Manipulating the KUSD price oracle

Figure 11. Manipulating the KUSD price oracle


Attacker Contract #1 manipulated the KUSD price oracle in this attack step, as described in Figure 11. This attack step can be briefly described step-by-step as follows.

  1. Attacker Contract #1 executed the update function of the StablePoolOracle contract.

  2. StablePoolOracle contract invoked the calculateSwap function of the Dopple Swap contract.

  3. Dopple Swap contract called the calculateSwap function of the Dopple Tri Pool contract.

  4. Dopple Tri Pool contract calculated the quoted (spot) price of 1 KUSD for USDC. In other words, the market price of 1 KUSD in exchange for USDC was calculated. Since the USDC reserve in the pool was drained in the previous attack step, the calculated quoted price was $0.145838385420664696 as a result.

  5. Dopple Tri Pool contract returned the calculated quoted price, $0.145838385420664696, to the Dopple Swap contract.

  6. Dopple Swap contract returned the quoted price to the StablePoolOracle contract.

  7. StablePoolOracle contract updated the KUSD-TWAP parameter based on the received quoted price. The TWAP parameter was manipulated from $0.996647861433978465 to $0.145904042239672548 (≈85.3605% decrease).


Figure 12. The update function of the StablePoolOracle contract

Figure 12. The update function of the StablePoolOracle contract


Let’s understand how the KUSD-TWAP parameter was manipulated in detail. The update function of the StablePoolOracle contract is presented in Figure 12.

The function invoked the calculateSwap function of the Dopple Swap contract (line no’s. 72–76), and the quoted price, $0.145838385420664696, was returned. Later, the received quoted price was used to calculate the TWAP parameter (line no’s. 79–87). As a result, the TWAP parameter was changed from $0.996647861433978465 to $0.145904042239672548.

From our analysis, we found that the TWAP parameter was calculated from the quoted price, the market price of 1 KUSD in exchange for USDC in the Dopple Tri Pool. And, this was the root cause of the price oracle manipulation.

Furthermore, we also found that the update function allowed anyone to execute freely. After draining the USDC reserve, Attacker Contract #1 could execute the update function to alter the TWAP parameter.

In summary, Attacker Contract #1 successfully manipulated the KUSD price oracle in this attack step because the update function of the StablePoolOracle contract calculated the TWAP parameter based on the quoted price being manipulated in the previous attack step.


Step 5: Redeeming the Minted Fractional Synthetic Asset tXAU


Figure 13. Redeeming the minted fractional synthetic asset tXAU

Figure 13. Redeeming the minted fractional synthetic asset tXAU


After manipulating the KUSD price oracle, Attacker Contract #1 redeemed the previously minted tXAU tokens in this attack step. Figure 13 pictures the step-by-step redemption process as follows.

  1. Attacker Contract #1 transferred the previously minted 606.195630970930049706 tXAU to another contract Attacker Contract #2.

  2. Attacker Contract #2 invoked the redeemFractionalSynth function of the SyntheticPool contract to redeem the previously received tXAU tokens. Remarkably, the attacker used the Attacker Contract #2 to perform this redemption step to bypass the flash loan prevention mechanism of the redeemFractionalSynth function. We will explain the bypass again later.

  3. SyntheticPool contract contacted the StablePoolOracle contract to read the collateral (KUSD) price from the manipulated TWAP.

  4. StablePoolOracle contract returned the collateral price, $0.145904042239672548, to the SyntheticPool contract.

  5. SyntheticPool contract used the obtained collateral price to calculate TWX and KUSD tokens to retrieve after the redemption. Consequently, SyntheticPool contract minted 6,830,114.251646252786696562 TWX for Attacker Contract #2. Next, SyntheticPool contract unlocked and transferred 910,584.086600497979460971 KUSD from the CollateralReserve contract to Attacker Contract #2.

  6. Attacker Contract #2 transferred the obtained 6,830,114.251646252786696562 TWX and 910,584.086600497979460971 KUSD to Attacker Contract #1.


Figure 14. The redeemFractionalSynth function of the SyntheticPool contract

Figure 14. The redeemFractionalSynth function of the SyntheticPool contract


The redeemFractionalSynth function of the SyntheticPool contract is shown in Figure 14. As mentioned earlier, this function was invoked by Attacker Contract #2 to bypass the flash loan prevention check in line no. 315.

Since Attacker Contract #1 had executed the mintFractionalSynth function in the attack step 2 previously, Attacker Contract #1 could not execute the redeemFractionalSynth function in the same block. Thus, the attacker used Attacker Contract #2 to redeem the minted tXAU tokens instead.

The following gathers the highlighted states at the redemption time.

  • TWX price: $0.135709613434781496 (unchanged)

  • KUSD price: $0.145904042239672548 (changed from $0.996647861433978465)

  • tXAU price: $1,760.555 (unchanged)

  • Collateral ratio: 12.536% (changed from 85.75%)


The redeemFractionalSynth function did some calculations (line no’s. 329–345). And, the following highlights some computation results.

  • tXAU amount to burn: 606.195630970930049706 tXAU

  • Redemption fee: 4.243369416796510348 tXAU

  • TWX amount to mint: 6,830,114.251646252786696562 TWX

  • KUSD amount to unlock and transfer: 910,584.086600497979460971 KUSD


After the calculation process, the redeemFractionalSynth function burned 606.195630970930049706 tXAU from Attacker Contract #2 (line no. 365). Then, the function minted 4.243369416796510348 tXAU as the redemption fee (line no. 366).

Next, the function minted 6,830,114.251646252786696562 TWX for Attacker Contract #2 (line no. 367). As you can see, an excessive amount of TWX was minted in this step.

Lastly, the function unlocked and transferred 910,584.086600497979460971 KUSD from the CollateralReserve contract to Attacker Contract #2 (line no’s. 368–372).

We found the root cause residing in the following functions: collateralReserve.getECR (line no. 318) and getCollateralPrice (line no. 327). These functions used the manipulated TWAP as the KUSD price oracle.


Figure 15. The getCollateralPrice function of the SyntheticPool contract

Figure 15. The getCollateralPrice function of the SyntheticPool contract


Figure 15 displays the getCollateralPrice function, one of two associated functions that used the manipulated TWAP as the KUSD (collateral) price oracle.

Figure 16. Tokens transferred while redeeming the tXAU

Figure 16. Tokens transferred while redeeming the tXAU


The evidence of tokens transferred during the tXAU redemption process is shown in Figure 16.

In summary, the attacker programmed the Attacker Contract #2 to redeem the previously minted tXAU tokens to bypass the flash loan prevention mechanism in this attack step.

During the redemption process, the manipulated TWAP was used as the KUSD (collateral) price oracle that resulted in minting the excessive TWX tokens.


Step 6: Paying Back the Loans and Taking the Profit


Figure 17. Paying back the loans and taking the profit

Figure 17. Paying back the loans and taking the profit


The final step is paying back the borrowed loans and taking the profit from the rest tokens, as described in Figure 17. Attacker Contract #1 performed several swaps to avoid the price impact and then paid back the loans borrowed from Twindex and LatteSwap pools (Steps 1–5).

After that, Attacker Contract #1 swapped the remaining tokens to 142,602.550550984220288325 BUSD and 144,416.90793727401988751 USDC (Step 6), and transferred the tokens to the attacker (Step 7).

Figure 18. Tokens transferred while paying back the loans and taking the profit

Figure 18. Tokens transferred while paying back the loans and taking the profit


Figure 18 displays the evidence of tokens transferred during paying back the loans and taking the profit.

In summary, Attacker Contract #1 paid back all loans borrowed from Twindex and LatteSwap pools. Eventually, the attacker took the remaining tokens as the profit.


.    .    .


We found three significant bugs in Twindex’s smart contracts that enabled the attack incident from our investigation.

First bug: the KUSD price oracle (TWAP) in the StablePoolOracle contract was implemented incorrectly. The TWAP used the spot price (quoted price) of KUSD instead of a cumulative price which is more reliable and resilient to flash loan and price manipulation attacks.

We recommend re-designing the affected price oracle using the cumulative price like Uniswap’s solution or considering using more reliable off-chain price oracle solutions (if necessary).

Second bug: the update function of the StablePoolOracle contract allowed anyone to execute for updating the TWAP oracle freely.

We recommend applying a whitelist. Only the whitelisted addresses can successfully execute the update function.

Third bug: the flash loan prevention mechanism (i.e., the state variable lastAction) in the SyntheticPool contract had design flaws, which can easily bypass by another Attacker contract.

We recommend re-designing the affected prevention mechanism and making sure that the new design is security-proof.


.    .    .

Summary


The attacker used the flash loan attack to exploit Twindex’s price oracle. Our investigation found some flaws in the design and implementation of the platform’s price oracle, access control, and flash loan prevention mechanisms. To address issues, we also recommend remediation solutions in this report.


.    .    .

Author Details


Phuwanai Thummavet (serial-coder), Lead Blockchain Security Auditor and Consultant | Blockchain Architect and Developer.

See the author’s profile.


.    .    .

About Valix Consulting



Valix Consulting is a blockchain and smart contract security firm offering a wide range of cybersecurity consulting services. Our specialists, combined with technical expertise with industry knowledge and support staff, strive to deliver consistently superior quality services.

For any business inquiries, please get in touch with us via Twitter, Facebook, or info@valix.io.


.    .    .

Originally published in Valix Consulting’s Medium.