ALTAVA Bug Report

The ALTAVA Group is a company focusing on the intersection of the metaverse, web3 and fashion. In August 2022, the ALTAVA team was planning an airdrop to owners of their "second skin" NFT collection. According to this google sheet, their plan was to airdrop 500k of their native TAVA token:
ALTAVA Airdrop Distribution
As I was doing some random browsing through recently verified contracts on etherscan, I came across the ALTAVA team's smart contracts. After I realized that one of their contracts held 270k TAVA token (worth over $300k USD at the time), I started looking through their code.
The team had set-up their claiming function using a merkle tree approach, which is quite common. For these types of airdrops, very little information needs to be stored on-chain (only the root of the merkle tree), so the initial transaction fees are small. While I was working at Paradigm, I actually made a CTF puzzle relating to merkle airdrops. Ivan Kuznetsov (aka jeiwan.eth) has a great writeup on the CTF puzzle and how merkle distribution works in general. If you understand the idea, you will probably see the critical bug in the following code:
function ClaimToStaking(
    uint256 _roothashIdx,
    bytes32[] memory _proof,
    bool[] memory _proofFlags,
    bytes32[] memory _leaves
) external override nonReentrant {
    bytes32 root = rootHashInfos[_roothashIdx].rootHash;
    require(MerkleProof.multiProofVerify(_proof, _proofFlags, root, _leaves));
    uint256 _amount = compensationAmount.mul(_leaves.length);
    uint256 _amountReceived = receivedInfos[_msgSender()][_roothashIdx].amountReceived;
    require(_amount > _amountReceived,"ClaimToStaking_ERR02");

    uint256 _amountReceive = GetTokensCurrentlyReciveable(_roothashIdx ,_leaves.length);
    require(_amountReceive > 0,"ClaimToStaking_ERR03");

    IERC20(tavaTokenAddress).transfer(_msgSender(), _amountReceive);

    receivedInfos[_msgSender()][_roothashIdx] = receivedInfo(
        _roothashIdx,
        (_amountReceived + _amountReceive)
    );
    emit claimToStaking(_msgSender(), _roothashIdx, _amountReceive, block.timestamp);
}

The problem is quite simple: instead of computing the leaf nodes on-chain based on user input, the function lets the user pass in the leaf nodes themselves. The Uniswap MerkleDistributor contract shows an example of a correct implemention, where leaf nodes are constructed based on msg.sender, the index in the merkle tree, and the amount of tokens:
function claim(
    uint256 index, 
    address account, 
    uint256 amount, 
    bytes32[] calldata merkleProof
) external override {
    require(!isClaimed(index));

    // Verify the merkle proof.
    bytes32 node = keccak256(abi.encodePacked(index, account, amount));
    require(MerkleProof.verify(merkleProof, merkleRoot, node));

    // Mark it claimed and send the token.
    _setClaimed(index);
    require(IERC20(token).transfer(account, amount));

    emit Claimed(index, account, amount);
}  

Merkle trees are useful because their security relies on the difficulty of computing preimages for a given hash output. The ALTAVA contract allowed the user to provide the output itself, so this key security feature could be bypassed. An attacker could call the ALTAVA contract with _leaves = [root] and _proof = [], and the contract will believe the attacker's address exists in the merkle tree. The attacker can repeat this multiple times (with some arbitrary msg.sender addresses) to drain the entire contract for a $300k profit.
After alerting the ALTAVA team of the vulnerability, they quickly moved the TAVA tokens back to an EOA. In order to fix the contract, I recommended constructing the leaf nodes on-chain so an attacker could not bypass the merkle verification.


October 9, 2022