Merlin Lab — Incident Case Analysis
Tags: Merlin, Solidity, Bsc, Security, Merlinlab

Incident Analysis

In this section, we give you a detailed step-by-step analysis of the incident. One of the 25 attack transactions was picked to investigate, transaction id: 0x31fdf621f60684579de00f30b33fa051c19f1fd45d891c817e2a3888a9b75726. The attacker, in addition, used the same technique for the other transactions.

Figure 2. Overview of Smart Contracts interaction

The attacker exploited the attack by deploying a custom smart contract as an attack automated tool called Attacker Contract.

As illustrated in Figure 2, the attacker invoked the deployed Attacker Contract. The Attacker Contract then atomically executed sequential calls to other related contracts to profit the attacker within a single transaction.

The attack transaction can be broken down into seven steps as follows.

Step 1: Deposited 0.1 WBNB to Merlin’s Alpaca Vault

Figure 3. The _deposit function of Merlin Contract

The attack began by the attacker made a call to the Attacker Contract. Then, the Attacker Contract transferred 0.1 WBNB to the _deposit function of Merlin Proxy as Step 1.

The function then deposited the received 0.1 WBNB to the Alpaca Vault to mint ibBNB tokens for the Attacker Contract (line no. 201). The function then calculated shares and principal variables (line no’s. 214–218) reflecting a deposit portion of the Attacker Contract in the MerlinStrategyAlpacaBNB’s pool.

Note, the ibBNB is the Alpaca’s interest-bearing token used to keep track of the users’ deposited funds and any interest earned.

Step 2: Transferred 546.712 WBNB to Merlin Proxy Contract

Figure 4. The transfer function of WBNB Contract

Then, the Attacker Contract called the transfer function on WBNB Contract to transfer 546.712 WBNB to Merlin Proxy Contract as Step 2.

Step 3: Transferred 1 ALPACA to Merlin Proxy Contract

Figure 5. The transfer function of Alpaca Contract

Next, the Attacker Contract called the transfer function on Alpaca Contract to transfer 1 ALPACA to Merlin Proxy Contract as Step 3.

Step 4: Harvested the ALPACA reward

Figure 6. The _harvest and _tryReinvest functions of Merlin Contract

In Step 4, the Attacker Contract executed the _harvest function to harvest the ALPACA reward. The _harvest function has a staking check (line no. 192). However, since the Attacker Contract has previously deposited 0.1 WBNB in Step 1, the deposit bypassed the staking check logic here.

Since the deposit of 0.1 WBNB to the Alpaca Vault in Step 1 has just performed, within the same transaction as Step 4, there was no ALPACA reward earned (line no. 194). However, the attacker’s objective for executing this step was to invoke the _tryReinvest function (line no. 196).

Since the _tryReinvest function has the reward amount check logic in line no. 209, the control flow would pass this check if and only if the rewardTokenAmount is more than or equal to the rewardTokenSwapThreshold .

Because the function considered the deposit of 1 ALPACA in Step 3 as a reward, rewardTokenAmount , the condition check was bypassed. Next, the function swapped the deposited 1 ALPACA for 0.001424 WBNB via PancakeSwap Router (line no. 212).

Subsequently, once the function looked up for its balance, the function found 546.713 WBNB (i.e., 546.712 WBNB from the transfer in Step 2 + 0.001424 WBNB from the reward swap in line no. 212). Surprisingly, the function determined this balance as stakingTokenAmount (line no. 215).

Eventually, the function deposited the stakingTokenAmount , 546.713 WBNB, to the Alpaca Vault, and the addition ibBNB tokens were minted for the Attacker Contract (line no. 221).

Step 5: Obtained incorrectly minted MERL reward

Figure 7. The _getRewards function of Merlin Contract

Later, the Attacker Contract invoked the _getRewards function as Step 5. Intriguingly, the function considered the deposited 546.713 WBNB in the previous step as the earnedWantTokenAmount (line no. 276).

In line no. 299, the earnedWantTokenAmount was passed as an argument into the function _addProfitReward , and this is where the incorrect MERL tokens were minted as rewards by mistake. We will have a look into the details of this function later on.

After the execution in line no. 299 succeeded, the earnedWantTokenAmount would hold some profit reward of 382.7 WBNB. This reward was then transferred to the Attacker Contract (line no. 303).

Figure 8. The _addProfitReward function of Merlin Contract

Let’s investigate how the incorrect MERL tokens were mistakenly minted. The reward of 546.713 WBNB was passed as the amount argument into the _addProfitReward function. The amount was used to calculate a performance fee (line no. 236), which was 164014164406883244159 in this case. After that, the performance fee was passed into the mintFor function (line no. 245).

Figure 9. The mintFor function of Merlin Contract

In the mintFor function, the new _performanceFee was calculated (line no’s. 211–212). The contribution parameter was computed from the _performanceFee (line no’s. 214–215). Consequently, the resulting contribution was used by the amountMerlinToMint function to calculate the amount of MERL tokens to mint as rewards (line no. 224). The incorrect 5,625.685 MERL were minted for the Attacker Contract (line no. 226).

Figure 10. The amountMerlinToMint function of Merlin Contract

And, the amountMerlinToMint function was the root cause of the exploit. This function calculated the amount of MERL tokens that would be minted for the Attacker Contract as rewards.

The following equation was being used:

mintMerlin = bnbProfit * merlinPerProfitBNB / 10^18

Where the merlinPerProfitBNB had a fixed value of 350000000000000000, the mintMerlin was 5,625.685, which considered an invalid value as the fixed value of the merlinPerProfitBNB did not reflect the actual price of MERL comparing to the price of BNB at that moment.

Step 6: Withdrew the deposited 0.1 WBNB

Figure 11. The withdrawAll function of Merlin Contract

After the incorrect 5,625.685 MERL was taken, the Attacker Contract invoked the withdrawAll function as Step 6. This function withdrew the deposit of 0.1 WBNB (in Step 1) from Alpaca Vault (line no. 157). The deposit amount deducted from the withdrawalFee and the performanceFee , 0.0995 WBNB, was transferred to the Attacker Contract (line no. 175).

Step 7: Swapped the incorrectly minted 5,625.685 MERL for 246.289 WBNB

Figure 12. The swapExactTokensForTokens function of PancakeSwap Contract

Step 7 was performed as the final step. The Attacker Contract invoked a call to the swapExactTokensForTokens function to swap the incorrectly minted 5,625.685 MERL for 246.289 WBNB through PancakeSwap Router.

Profit gained from this transaction

amountIn = 382.7 WBNB (Step 5) +

0.099 WBNB (Step 6) +

246.289 WBNB (Step 7)

= 629.088 WBNB amountOut = 0.1 WBNB (Step 1) +

546.712 WBNB (Step 2) +

0.001 WBNB (Step 3)

= 546.813 WBNB txFee = 0.012 WBNB profitGained = amountIn - amountOut - txFee

= 629.088 WBNB - 546.813 WBNB - 0.012 WBNB

= 82.263 WBNB

The attacker gained a profit from the considered transaction of 82.263 WBNB.