Skip to content

floranguyen0/tranchess-vulnerability-disclosure

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 

Repository files navigation

Vulnerability disclosure

I was searching on Immunefi for projects that piqued my interest, scanning contract after contract, the Tranchess protocol caught my eye. It has every component developed from the ground up, with quite a unique implementation on the risk/return matrix compared to other yield-farming protocols. This uniqueness of the codebase steered me to anticipate the presence of a protocol-specific type of bug that oftentimes leads to surprising damage.

Summary

  • On Oct. 30, 2023, at 09:50:00 UTC, an attack vector in the ShareStaking contract was disclosed. It was mitigated by the Tranchess team with a temporary fix within 18 hours of its receipt, the permanent solution was deployed 6 days later. User assets remain secure and there were no losses.
  • As part of the security process, the Tranchess team has acknowledged and rewarded me with the maximum bounty of $200,000 from the Tranchess treasury.
  • The vulnerability originated from the omission of the _checkpoint() function, leading to a potential mismatch of the tokens' balance the ShareStaking contract holds.
  • An important note from the vulnerability for developers and security researchers is to pay close attention to any gas optimization techniques. A seemingly innocuous method to cut execution costs can at times cause dangerous behaviours.

What is Tranchess and how does it work?

Tranchess is a yield-enhancing asset tracker protocol with varied risk-return solutions. It provides a different risk/return matrix out of a single main fund that tracks a specific underlying asset (e.g. BTC, ETH, BNB) or a basket of crypto assets.

The main fund is an asset tracking index fund. Queen’s Net Asset Value (NAV) tracks the underlying asset's price on a fully correlated basis with deduction of protocol fees. Token Queen can be further split into/merge from two sub-tranches, token Bishop and token Rook. Token Rook leverages exposure to the main fund without forced liquidation risk. Token Bishop provides BUSD yield at a variable interest rate.

Details of the vulnerability

ShareStaking.deposit()

The deposit() function enables users to stake their Queen/Bishop/Rook tokens. The crucial variable, spareAmount within this function is the amount of tokens the ShareStaking contract has received for a given deposit, which is determined by calculating the disparity between token total supply of the ShareStaking contract and the actual token balance the contract holds.

    function deposit(uint256 tranche, uint256 amount, address recipient, uint256 version) external {
        _checkpoint(version);
        _userCheckpoint(recipient, version);
        _balances[recipient][tranche] = _balances[recipient][tranche].add(amount);
        uint256 oldTotalSupply = _totalSupplies[tranche];
        _totalSupplies[tranche] = oldTotalSupply.add(amount);
        _updateWorkingBalance(recipient, version);
        uint256 spareAmount = fund.trancheBalanceOf(tranche, address(this)).sub(oldTotalSupply);
        if (spareAmount < amount) {
            // Retain the rest of share token (version is checked by the fund)
            fund.trancheTransferFrom(
                tranche,
                msg.sender,
                address(this),
                amount - spareAmount,
                version
            );
        } else {
            require(version == _fundRebalanceSize(), "Invalid version");
        }
        emit Deposited(tranche, recipient, amount);
    }

Rebalance mechanism and ShareStaking._checkpoint()

A rebalance can be initiated in the FundV3 contract when the Fair Value ratio (ROOK/BISHOP) is below 0.5 or over 2. A rebalance will reset this ratio back to 1. Following a rebalance event, the balance of tokens in the ShareStaking contract will change due to the adjustment of the fair value of token BISHOP and ROOK. This change involves an increase in the Q balance (information about the additional Q amount can be found here).

The vulnerability stems from _checkpoint() function, which is responsible for making a global reward checkpoint and updating the token total supplies based on the latest rebalance version.

The _checkpoint() will be skipped if we have called _checkpoint() in the same block previously.

    function _checkpoint(uint256 rebalanceSize) private {
        uint256 timestamp = _checkpointTimestamp;
        if (timestamp >= block.timestamp) {
            return;
        }
		...
    }

Exploiting the vulnerability

In the transaction that triggers a rebalance, if the attacker calls _checkpoint earlier and causes the subsequent checkpoint() within deposit() to be skipped, the spareAmount value for the Queen tranche would become the amount of Queen tokens that is drainable from the ShareStaking contract. This happens because the Queen total supply has not been synchronized with the ShareStaking contract's Queen balance of the most recent rebalance version stored in the FundV3 contract.

If the attacker has the fund, they can obtain Bishop and Rook tokens to deposit them into the ShareStaking contract before the rebalance in order to increase the spareAmountvalue (since the more Bishop and Rook tokens the ShareStaking contract holds, the more Queen tokens it will receive after a rebalance).

Otherwise, the summarized attack steps are as follows:

  1. The attacker monitors the underlying price and waits for the time when they can initiate a rebalance by calling settle() in the FundV3 contract, potentially employing frontrunning and private transaction services (accessible at https://bloxroute.com/ for BSC chain) to execute the rebalance before the Tranchess team does.

  2. When the price reaches the rebalance threshold at 14:00 UTC (the settlement time), the attacker calls claimableRewards() in the ShareStaking contract with the argument of any address other than the attacker's address (to prevent the transaction from reverting later due to subtraction overflow). The purpose of this call is to invoke _checkpoint() in the ShareStaking contract, causing it to skip the subsequent _checkpoint() when we invoke later in the same transaction in deposit().

  3. The attacker proceeds to call settle() in the FundV3 contract, triggering a rebalance.

  4. The attacker calls deposit() function in the ShareStaking contract to deposit tranche Q, with the amount argument being precomputed and equal to the spareAmount value within the deposit() function.

  5. Finally, the attacker withdraws and redeem the drained Q to obtain underlying tokens, successfully drains users' funds.

Proof of concept

Check out the POC here

Impact

Whenever the condition is right for a rebalance to happen, an attacker can directly steal funds from existing stakers. The impact of this attack depends on the size of the attacker's fund:

  • If the attacker possesses a fund, the maximum total loss is approximately 815.1 BTC and 1438.5 ETH on BSC chain, based on the on-chain funds at the time of reporting.

  • If the attacker does not possess any fund, the total loss is approximately 46.8 BTC and 156.2 ETH on BSC chain, based on the on-chain funds at the time of reporting.

Following the attack, the ShareStaking contract within the Tranchess protocol will become insolvent, and the deposit() function in the contract will always revert due to subtraction overflow.

Additionally, the accounting of _workingSupply and _totalSupplies for the three tranches in the ShareStaking will be perpetually miscalculated.

Mitigation

For a detailed explanation of the mitigation, please refer to the Tranchess team's publication.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published