Description
In StabilityPool::calculateRaacRewards
the function uses raacToken.balanceOf(address(this))
to calculate the rewards for a user during withdrawal, this leads to an unwanted behavior, considering the contract allows depositing RAACToken
via StabilityPool::depositRAACFromPool
.
Vulnerable Code
StabilityPool::calculateRaacRewards
:
function calculateRaacRewards(address user) public view returns (uint256) {
uint256 userDeposit = userDeposits[user];
uint256 totalDeposits = deToken.totalSupply();
@> uint256 totalRewards = raacToken.balanceOf(address(this));
if (totalDeposits < 1e6) return 0;
return (totalRewards * userDeposit) / totalDeposits;
}
StabilityPool::depositRAACFromPool
:
function depositRAACFromPool(uint256 amount) external onlyLiquidityPool validAmount(amount) {
uint256 preBalance = raacToken.balanceOf(address(this));
raacToken.safeTransferFrom(msg.sender, address(this), amount);
uint256 postBalance = raacToken.balanceOf(address(this));
if (postBalance != preBalance + amount) revert InvalidTransfer();
emit RAACDepositedFromPool(msg.sender, amount);
}
As you can see, StabilityPool
expects deposits from the LiquidityPool
(implementation pending) for allocation towards the managers of the markets. The issue arising is that those deposits will be wrongfully distributed as rewards to the users, leaving the original intend behind.
PoC
Since the PoC is a foundry test I have added a Makefile at the end of this report to simplify installation for your convenience. Otherwise if console commands would be prefered:
First run: npm install --save-dev @nomicfoundation/hardhat-foundry
Second add: require("@nomicfoundation/hardhat-foundry");
on top of the Hardhat.Config
file in the projects root directory.
Third run: npx hardhat init-foundry
And lastly, you will encounter one of the mock contracts throwing an error during compilation, this error can be circumvented by commenting out the code in entirety (ReserveLibraryMocks.sol
).
And the test should be good to go:
After following above steps copy & paste the following code into ./test/invariant/PoC.t.sol
and run forge test --mt test_pocDepositsToStabilityPoolAreDistributedWrogfullyAsRewards -vv
pragma solidity ^0.8.0;
import {Test, console} from "forge-std/Test.sol";
import {StabilityPool} from "../../contracts/core/pools/StabilityPool/StabilityPool.sol";
import {LendingPool} from "../../contracts/core/pools/LendingPool/LendingPool.sol";
import {CrvUSDToken} from "../../contracts/mocks/core/tokens/crvUSDToken.sol";
import {RAACHousePrices} from "../../contracts/core/oracles/RAACHousePriceOracle.sol";
import {RAACNFT} from "../../contracts/core/tokens/RAACNFT.sol";
import {RToken} from "../../contracts/core/tokens/RToken.sol";
import {DebtToken} from "../../contracts/core/tokens/DebtToken.sol";
import {DEToken} from "../../contracts/core/tokens/DEToken.sol";
import {RAACToken} from "../../contracts/core/tokens/RAACToken.sol";
import {RAACMinter} from "../../contracts/core/minters/RAACMinter/RAACMinter.sol";
contract PoC is Test {
StabilityPool public stabilityPool;
LendingPool public lendingPool;
CrvUSDToken public crvusd;
RAACHousePrices public raacHousePrices;
RAACNFT public raacNFT;
RToken public rToken;
DebtToken public debtToken;
DEToken public deToken;
RAACToken public raacToken;
RAACMinter public raacMinter;
address owner;
address oracle;
address user1;
address user2;
address user3;
uint256 constant STARTING_TIME = 1641070800;
uint256 public currentBlockTimestamp;
uint256 constant WAD = 1e18;
uint256 constant RAY = 1e27;
function setUp() public {
vm.warp(STARTING_TIME);
currentBlockTimestamp = block.timestamp;
owner = address(this);
oracle = makeAddr("oracle");
user1 = makeAddr("user1");
user2 = makeAddr("user2");
user3 = makeAddr("user3");
uint256 initialPrimeRate = 0.1e27;
raacHousePrices = new RAACHousePrices(owner);
vm.prank(owner);
raacHousePrices.setOracle(oracle);
crvusd = new CrvUSDToken(owner);
raacNFT = new RAACNFT(address(crvusd), address(raacHousePrices), owner);
rToken = new RToken("RToken", "RToken", owner, address(crvusd));
debtToken = new DebtToken("DebtToken", "DT", owner);
deToken = new DEToken("DEToken", "DEToken", owner, address(rToken));
vm.prank(owner);
crvusd.setMinter(owner);
vm.prank(owner);
lendingPool = new LendingPool(
address(crvusd),
address(rToken),
address(debtToken),
address(raacNFT),
address(raacHousePrices),
initialPrimeRate
);
rToken.setReservePool(address(lendingPool));
debtToken.setReservePool(address(lendingPool));
rToken.transferOwnership(address(lendingPool));
debtToken.transferOwnership(address(lendingPool));
stabilityPool = new StabilityPool(address(owner));
deToken.setStabilityPool(address(stabilityPool));
raacToken = new RAACToken(owner, 0, 0);
raacMinter = new RAACMinter(address(raacToken), address(stabilityPool), address(lendingPool), owner);
stabilityPool.initialize(address(rToken), address(deToken), address(raacToken), address(raacMinter), address(crvusd), address(lendingPool));
vm.prank(owner);
raacToken.setMinter(address(raacMinter));
attacker = new Attacker(address(raacNFT));
crvusd.mint(address(attacker), type(uint128).max);
crvusd.mint(user1, type(uint128).max);
crvusd.mint(user2, type(uint128).max);
crvusd.mint(user3, type(uint128).max);
}
function test_pocDepositsToStabilityPoolAreDistributedWrogfullyAsRewards() public {
vm.startPrank(user1);
crvusd.approve(address(lendingPool), type(uint256).max);
rToken.approve(address(stabilityPool), type(uint256).max);
lendingPool.deposit(10e18);
stabilityPool.deposit(10e18);
vm.stopPrank();
vm.startPrank(user2);
crvusd.approve(address(lendingPool), type(uint256).max);
rToken.approve(address(stabilityPool), type(uint256).max);
lendingPool.deposit(10e18);
stabilityPool.deposit(10e18);
vm.stopPrank();
vm.prank(address(raacMinter));
raacToken.mint(owner, 10e18);
vm.startPrank(owner);
raacToken.manageWhitelist(address(stabilityPool), true);
raacToken.manageWhitelist(address(raacMinter), true);
raacToken.manageWhitelist(owner, true);
stabilityPool.setLiquidityPool(owner);
raacToken.approve(address(stabilityPool), 10e18);
stabilityPool.depositRAACFromPool(10e18);
vm.stopPrank();
assertEq(raacToken.balanceOf(address(stabilityPool)), 10e18);
vm.prank(user1);
stabilityPool.withdraw(10e18);
vm.prank(user2);
stabilityPool.withdraw(10e18);
assertEq(raacToken.balanceOf(address(stabilityPool)), 0);
}
}
As you can easily see running the test above, all RAAC Tokens sent to the Stability Pool are now withdrawn by the 2 users.
Impact
Distributing accidentally those funds, meant for Manager/Market allocations, leaves the functionality of any following logic useless by overpaying the users. The likelihood is High since the only precondition was has to be met is, that RAAC needs to be deposited.
Also, the misdistribution of funds towards the users in this case (or a malicious actor who frontruns the reward distribution as mentioned in an earlier report), causes a High impact to the system since the funds for implemented logic are simply not there and furthermore not fairly distributed.
Therefore it is a total severity of High.
Tools Used
Manual Review
Recommended Mitigation
Disconnect from balanceOf()
accounting during reward distribution by tracking uint256 totalRewards
with received minted RAAC tokens - Distributed RAAC Tokens or maybe better, track deposits from Liquidity Pool seperatly and deducting them before distribution:
+ uint256 raacDepositedFromPool;
function depositRAACFromPool(uint256 amount) external onlyLiquidityPool validAmount(amount) {
uint256 preBalance = raacToken.balanceOf(address(this));
raacToken.safeTransferFrom(msg.sender, address(this), amount);
uint256 postBalance = raacToken.balanceOf(address(this));
if (postBalance != preBalance + amount) revert InvalidTransfer();
+ raacDepositedFromPool += amount;
// TODO: Logic for distributing to managers based on allocation
emit RAACDepositedFromPool(msg.sender, amount);
}
function calculateRaacRewards(address user) public view returns (uint256) {
uint256 userDeposit = userDeposits[user];
uint256 totalDeposits = deToken.totalSupply();
- uint256 totalRewards = raacToken.balanceOf(address(this));
+ uint256 totalRewards = raacToken.balanceOf(address(this)) - raacDepositedFromPool;
if (totalDeposits < 1e6) return 0;
return (totalRewards * userDeposit) / totalDeposits;
}
Appendix
Copy the following import into your Hardhat.Config
file in the projects root dir:
require("@nomicfoundation/hardhat-foundry");
Paste the following into a new file "Makefile" into the projects root directory:
.PHONY: install-foundry init-foundry all
install-foundry:
npm install --save-dev @nomicfoundation/hardhat-foundry
init-foundry: install-foundry
npx hardhat init-foundry
# Default target that runs everything in sequence
all: install-foundry init-foundry
And run make all