Summary
The StabilityPool contract inherits from ReentrancyGuard instead of ReentrancyGuardUpgradeable while being an upgradeable contract. This causes a storage collision risk since ReentrancyGuard uses a storage variable without storage gaps, potentially corrupting the contract's state variables during upgrades.
Vulnerability Details
The StabilityPool contract is an upgradeable contract but inherits from the non-upgradeable ReentrancyGuard. The issue arises because:
ReentrancyGuard has a state variable _status at slot 0 and StabilityPool has its own state variables starting from slot 1 (instead of slot 0 which is usually the intended for the base upgreadable smart contract)
Due to the inheritance, ReentrancyGuard's _status occupies slot 0, which should be reserved for StabilityPool's first variable.
When the contract is upgraded, if the new implementation changes the storage layout, it could lead to storage collisions
Looking at the storage layout:
abstract contract ReentrancyGuard {
uint256 private _status;
}
contract StabilityPool is IStabilityPool, Initializable, ReentrancyGuard, OwnableUpgradeable, PausableUpgradeable {
IRToken public rToken;
IDEToken public deToken;
This means the _status variable from ReentrancyGuard is overlapping with what should be the first state variable of StabilityPool.
A minor issue (can be ignored) - the ReentrancyGuard constructor is not effective as it being called in the context of the implementation and not the actual proxy. (this is why when we query it in the test in the PoC below we get 0) - NO real impact here since the _nonReentrantBefore in the nonReentrant modifier set it to NOT_ENTERED (2) anyways. but still something to take note of as initially there will be inconsistency btn implementation and the proxy.
constructor() {
_status = NOT_ENTERED;
}
On the other hand ReentrancyGuardUpgradeable uses custom storage slots which is better for avoiding such issues and a perfect fit for upgreadeable contracts.
struct ReentrancyGuardStorage {
uint256 _status;
}
bytes32 private constant ReentrancyGuardStorageLocation = 0x9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00;
In conclusion the issue is that the StabilityPool inherited from ReentrancyGuard instead of ReentrancyGuardUpgradeable there fore causing issues.
PoC
pragma solidity ^0.8.19;
import {Test, console} from "forge-std/Test.sol";
import {StabilityPool} from "contracts/core/pools/StabilityPool/StabilityPool.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) {}
}
contract StorageCorruption is Test {
address private alice = makeAddr("alice");
address private bob = makeAddr("bob");
StabilityPool private stabilityPool;
StabilityPool private implementation;
ERC1967Proxy private proxy;
address private mockRToken = address(new MockERC20("RToken"));
address private mockDeToken = address(new MockERC20("DeToken"));
address private mockRaacToken = address(new MockERC20("RaacToken"));
address private mockCrvUSDToken = address(new MockERC20("crvToken"));
address private mockRaacMinter = makeAddr("raac_minter");
address private mockLendingPool = makeAddr("lending_pool");
function setUp() public {
implementation = new StabilityPool(address(this));
bytes memory initData = abi.encodeWithSelector(
StabilityPool.initialize.selector,
mockRToken,
mockDeToken,
mockRaacToken,
mockRaacMinter,
mockCrvUSDToken,
mockLendingPool
);
proxy = new ERC1967Proxy(address(implementation), initData);
stabilityPool = StabilityPool(address(proxy));
}
function testSlots() public view {
bytes32 value = vm.load(address(stabilityPool), bytes32(0));
assertEq(uint256(value), 0);
for (uint256 i = 1; i < 5; i++) {
value = vm.load(address(stabilityPool), bytes32(i));
if (i == 1) assertEq(address(uint160(uint256(value))), mockRToken);
else if (i == 2) assertEq(address(uint160(uint256(value))), mockDeToken);
else if (i == 3) assertEq(address(uint160(uint256(value))), mockRaacToken);
else if (i == 4) assertEq(address(uint160(uint256(value))), mockLendingPool);
}
}
}
Impact
Storage corruption when upgrading.
Tools Used
Manual Review
Foundry Test Framework
Recommendations
Replace ReentrancyGuard with ReentrancyGuardUpgradeable:
- import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+ import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
- contract StabilityPool is IStabilityPool, Initializable, ReentrancyGuard, OwnableUpgradeable, PausableUpgradeable {
+ contract StabilityPool is IStabilityPool, Initializable, ReentrancyGuardUpgradeable, OwnableUpgradeable, PausableUpgradeable {
function initialize(...) public initializer {
+ __ReentrancyGuard_init();
...
}
}