Summary
The depositRAACFromPool
function in StabilityPool fails to account for RAAC token's fee-on-transfer mechanism, causing all deposits to revert due to a strict balance check.
Vulnerability Details
The depositRAACFromPool
function implements a balance check to verify the exact amount of RAAC tokens received. However, RAAC token implements both swap and burn taxes through its _update
function which is called on every transfer;
The RAAC token applies: A swap tax (default 1%), A burn tax (default 0.5%) thus Total tax of 1.5% on transfers. In RAACToken contract;
function _update(address from, address to, uint256 amount) internal virtual override {
uint256 totalTax = amount.percentMul(baseTax);
uint256 burnAmount = totalTax * burnTaxRate / baseTax;
super._update(from, feeCollector, totalTax - burnAmount);
super._update(from, address(0), burnAmount);
super._update(from, to, amount - totalTax);
}
When depositRAACFromPool
transfers RAAC tokens:
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();
Due to the tax mechanism, the actual received amount will always be less than the transferred amount:
For a transfer of 100 RAAC:
1.5 RAAC gets deducted (1% swap + 0.5% burn)
Only 98.5 RAAC arrives at the destination
Therefore, postBalance
will always be less than preBalance + amount
, causing the check to revert.
PoC
pragma solidity ^0.8.19;
import {Test, console} from "forge-std/Test.sol";
import {StabilityPool} from "contracts/core/pools/StabilityPool/StabilityPool.sol";
import {RAACToken} from "contracts/core/tokens/RAACToken.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MockERC20 is ERC20 {
constructor(string memory name) ERC20(name, name) {}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
}
contract RAACTokenFoTTest is Test {
address private liquidity_pool = makeAddr("liquidity_pool");
address private bob = makeAddr("bob");
StabilityPool private stabilityPool;
StabilityPool private implementation;
ERC1967Proxy private proxy;
RAACToken private raacToken;
address private mockRToken = address(new MockERC20("RToken"));
address private mockDeToken = address(new MockERC20("DeToken"));
address private mockCrvUSDToken = address(new MockERC20("crvToken"));
address private mockRaacMinter = makeAddr("raac_minter");
address private mockLendingPool = makeAddr("lending_pool");
function setUp() public {
raacToken = new RAACToken(
address(this),
100,
50
);
implementation = new StabilityPool(address(this));
bytes memory initData = abi.encodeWithSelector(
StabilityPool.initialize.selector,
mockRToken,
mockDeToken,
address(raacToken),
mockRaacMinter,
mockCrvUSDToken,
mockLendingPool
);
proxy = new ERC1967Proxy(address(implementation), initData);
stabilityPool = StabilityPool(address(proxy));
stabilityPool.setLiquidityPool(liquidity_pool);
raacToken.setMinter(mockRaacMinter);
vm.prank(mockRaacMinter);
raacToken.mint(liquidity_pool, 1000 ether);
raacToken.setFeeCollector(bob);
}
function testDepositRAACFromPoolReverts() public {
vm.startPrank(liquidity_pool);
raacToken.approve(address(stabilityPool), raacToken.balanceOf(liquidity_pool));
vm.expectRevert(abi.encodeWithSignature("InvalidTransfer()"));
stabilityPool.depositRAACFromPool(500 ether);
vm.stopPrank();
}
}
Impact
The depositRAACFromPool
function is completely unusable (DoS), breaking a core functionality of the protocol where the LiquidityPool should be able to provide RAAC rewards to the StabilityPool.
Tools Used
Manual Review
Recommendations
Modify the balance check to account for the balance differences rather than just the sent amount:
function depositRAACFromPool(uint256 amount) external onlyLiquidityPool validAmount(amount) {
uint256 preBalance = raacToken.balanceOf(address(this));
raacToken.safeTransferFrom(msg.sender, address(this), amount);
uint256 actualReceived = raacToken.balanceOf(address(this)) - preBalance;
if (actualReceived == 0) revert InvalidTransfer();
emit RAACDepositedFromPool(msg.sender, actualReceived);
}