FeeReceiver Write-Up

Between August 16th and 22nd 2022, $8,412.77 of Swapr protocol fees were stolen via an exploit from the Swapr feeReceiver on Gnosis Chain. This write-up covers the background and purpose of the feeReceiver contract, describes the attack in detail, and describes the mitigations taken and planned next steps for DXdao and the Swapr Squad. @0xVenky is credited with first identifying that the attack was happening.

FeeReceiver Background

Swapr is based on a fork of Uniswap V2. Uniswap and Swapr both have a protocol fee. Uniswap has yet to turn on this protocol fee. Swapr’s protocol fee has been active since deployment. In addition, unlike Uniswap, Swapr has the ability to adjust the protocol fee, and it has been active since deployment. Protocol fees come in the form of LP tokens from any given pair. Since Swapr can have any number of token pairs, each with their own LPs, this means a potentially onerous amount of LP tokens, which themselves contain two underlying tokens. Ultimately, DXdao, and eventually Swapr Guild, will want to be able to spend these funds, and therefore it is necessary to convert them into a commonly used currency such as ETH or stablecoins. The FeeReceiver contract was developed to assist in this process and also to make accounting of protocol fees easier by having a dedicated contract they are sent to separate from the treasury.

The FeeReceiver was the last contract developed before deploying Swapr. Development was a bit rushed and an audit was not commissioned. This was seen as acceptable because the amount of fees in the contracts would depend on the amount of Swapr volume and the fees could be taken out as they accumulated, thus limiting exposure. Smart contract audits and bug bounty programs can be costly and operating with a limited amount of funds at risk is an approach to establishing security without the upfront costs.

FeeRecevier updates were currently in the works to address limitations and expand functionality, but the issue exploited here was not known prior to the attack.

FeeReceiver Attack

@luzzifoss identified the hack sequence, and created a repository to reproduce the attack. Once the feeReceiver has been updated, this repository will be made public.

The attacker takes the following steps to “trick” the feeReceiver contract into sending LP tokens to the attacker.

  1. Pick a target pair to steal ( one of the pair tokens needs to be in the wrapped native currency of the chain, for example GNO-WXDAI )

  2. Create a pair on Swapr consisting of the target LP token ( GNO-XDAI following the above example ) and the chains native currency ( XDAI here ), and provide it with a very small amount of liquidity

  3. Create a “fake pair” contract that provides return values for the functions that will be called by the feeReceiver, namely token0(), token1(), and burn(address to). For token0() the attacker wants to make it return the address of the target LP token. token1() doesnt matter as long as something is returned, and the burn function needs to return the amount of LP token that the attacker plans to steal from the feeReceiver.

  4. Call takeProtocolFee passing in the fake pair created in step 3 as the pair to “collect”

Here we follow the code of the takeProtocolFee function in the feeReceiver with comments

// Pass in the address of the fake pair
function takeProtocolFee(IDXswapPair[] calldata pairs) external {
    for (uint i = 0; i < pairs.length; i++) {
        // here the fake pair returns the target LP token as token0
        address token0 = pairs[i].token0();
        address token1 = pairs[i].token1();
        pairs[i].transfer(address(pairs[i]), pairs[i].balanceOf(address(this)));
        // here the fake pair returns the amount of the target LP to steal, amount0
        (uint amount0, uint amount1) = pairs[i].burn(address(this));
        if (amount0 > 0)
            // This gets called which takes us to the next function below
            _takeETHorToken(token0, amount0);
        if (amount1 > 0)
            _takeETHorToken(token1, amount1);
    }
}


function _takeETHorToken(address token, uint amount) internal {
    if (token == WETH) {
        IWETH(WETH).withdraw(amount);
        TransferHelper.safeTransferETH(ethReceiver, amount);
    } else if (isContract(pairFor(token, WETH))) {
        // Since the attacker created a pair with the LP token and the native currency ( denoted as WETH here ), this path is taken, bringing us to the next function
        _swapTokensForETH(amount, token);
    } else {
        TransferHelper.safeTransfer(token, fallbackReceiver, amount);
    }
}


function _swapTokensForETH(uint amountIn, address fromToken)
    internal
{
    IDXswapPair pairToUse = IDXswapPair(pairFor(fromToken, WETH));
    
    (uint reserve0, uint reserve1,) = pairToUse.getReserves();
    (uint reserveIn, uint reserveOut) = fromToken < WETH ? (reserve0, reserve1) : (reserve1, reserve0);

    require(reserveIn > 0 && reserveOut > 0, 'DXswapFeeReceiver: INSUFFICIENT_LIQUIDITY');
    uint amountInWithFee = amountIn.mul(uint(10000).sub(pairToUse.swapFee()));
    uint numerator = amountInWithFee.mul(reserveOut);
    uint denominator = reserveIn.mul(10000).add(amountInWithFee);
    uint amountOut = numerator / denominator;
    
    // Here is where the LP tokens are sent from the feeReceiver to the Swapr pair created by attacker with a small amount of liquidity
    TransferHelper.safeTransfer(
        fromToken, address(pairToUse), amountIn
    );
    
    (uint amount0Out, uint amount1Out) = fromToken < WETH ? (uint(0), amountOut) : (amountOut, uint(0));
    
    pairToUse.swap(
        amount0Out, amount1Out, address(this), new bytes(0)
    );
    
    IWETH(WETH).withdraw(amountOut);
    TransferHelper.safeTransferETH(ethReceiver, amountOut);
}

Mitigation

After becoming aware of the attack, DXdao contributors called takeProtocolFee to remove most of the remaining fees, and are monitoring the fees so that they can be collected before additional attacks.

Work is being done to update the feeReceiver to make it resistant to this attack, to add slippage limits for the trading, and to add functionality around splitting protocol fees with other parties. Once this is ready, an updated feeReceiver will be deployed.

While work is being done on the feeReceiver, the following actions are potential ways to technically prevent the attack from happening:

  1. Update the contract and restrict calling takeProtocolFee to DXdao’s avatar
  2. Update feeReceiver address to be DXdao avatar
  3. Set protocol fee to zero
18 Likes

Thanks for the summary. It’s quite confusing. Glad to see this was caught earlier than later on.

2 Likes