Home

Awesome

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

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:

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.