The read-only reentrancy is a reentrancy scenario where a view function is reentered, which in most cases is unguarded as it does not modify the contract’s state. However, if the state is inconsistent, wrong values could be reported. Other protocols relying on a return value can be tricked into reading the wrong state to perform unwanted actions.
Protocols integrate with other DeFi protocols to read token prices or read prices of wrapped tokens minted on particular protocols. This is possible when any lending protocol supports the pool tokens from other protocols as collateral or allows staking. All these integrations will require reading pool token prices or wrapped token prices.
Vulnerable getSystemDebt function
/// @notice Get the sum of debt units assigned to all Cdps within eBTC system
/// @dev It is actually the `systemDebt` value of the ActivePool.
/// @return entireSystemDebt entire system debt accounting value
function getSystemDebt() public view returns (uint256 entireSystemDebt) {
return _getSystemDebt();
}
Proof of Concept
Attackers misuse these situations to perform read-only reentrancy attacks using price manipulation.
1. An attacker contract will call a contract that can provide reentrancy. Other protocols must also be reading some values from it. So, let’s call it a reentrant contract.
2. The reentrant contract will fall back to the attacker contract, and the fallback method logic will be executed.
3. The attacker contract will now call a target DeFi protocol contract, as nothing prevents it from calling it.
4. Target protocol, as assumed earlier, will be reading some value from the compromised reentrant contract.
5. By the time Step 3 and Step 4 finish, the attacker would have already exploited the target contract and will simply proceed to let Step 1 and Step 2 finish.
Exploit of getSystemDebt function in foundry
function test_Reen6() external {
address me = _utils.getNextUserAddress();
vm.prank(me);
cdpManager.getSystemDebt();
// fallback
payable(address(cdpManager)).call {value: 1e18} ("");
}
Mitigation methods vary and depend on whether a new contract is being written or whether a contract that’s already deployed is being read.
For new contracts, a read-only reentrancy guard needs to be added. This works by verifying whether the reentrancy guard has not been locked. If it has, then an error is thrown (for example, Euler Finance).
The lock can be made public so those contracts which are calling the contract can check if the contract is already in lock mode.
For the rest, a function with a reentrant modifier can be called first. If it fails, the contract will be in locked mode, and reading from it should be avoided.
Lines of code
https://github.com/code-423n4/2023-10-badger/blob/f2f2e2cf9965a1020661d179af46cb49e993cb7e/packages/contracts/contracts/CdpManager.sol#L567-L572
Vulnerability details
Impact
The read-only reentrancy is a reentrancy scenario where a view function is reentered, which in most cases is unguarded as it does not modify the contract’s state. However, if the state is inconsistent, wrong values could be reported. Other protocols relying on a return value can be tricked into reading the wrong state to perform unwanted actions.
Protocols integrate with other DeFi protocols to read token prices or read prices of wrapped tokens minted on particular protocols. This is possible when any lending protocol supports the pool tokens from other protocols as collateral or allows staking. All these integrations will require reading pool token prices or wrapped token prices.
Vulnerable getSystemDebt function
Proof of Concept
Attackers misuse these situations to perform read-only reentrancy attacks using price manipulation.
1.
An attacker contract will call a contract that can provide reentrancy. Other protocols must also be reading some values from it. So, let’s call it a reentrant contract.2.
The reentrant contract will fall back to the attacker contract, and the fallback method logic will be executed.3.
The attacker contract will now call a target DeFi protocol contract, as nothing prevents it from calling it.4.
Target protocol, as assumed earlier, will be reading some value from the compromised reentrant contract.5.
By the time Step 3 and Step 4 finish, the attacker would have already exploited the target contract and will simply proceed to let Step 1 and Step 2 finish.Exploit of getSystemDebt function in foundry
Log results in foundry
Tools Used
VS Code. Foundry.
Recommended Mitigation Steps
Mitigation methods vary and depend on whether a new contract is being written or whether a contract that’s already deployed is being read.
For new contracts, a read-only reentrancy guard needs to be added. This works by verifying whether the reentrancy guard has not been locked. If it has, then an error is thrown (for example, Euler Finance).
The lock can be made public so those contracts which are calling the contract can check if the contract is already in lock mode.
For the rest, a function with a reentrant modifier can be called first. If it fails, the contract will be in locked mode, and reading from it should be avoided.
Assessed type
Reentrancy