Malicious actors can exploit race condition in `RevenueTraderP1::returnTokens()` and `RevenueTraderP1::manageTokens()` to cause inconsistent state and potential loss of funds #62
The RevenueTraderP1 contract implements two key functions, returnTokens() and manageTokens(), which are responsible for managing token balances and executing trades. These functions operate on the same set of tokens but lack proper synchronization mechanisms, potentially leading to race conditions and inconsistent contract states.
The returnTokens() function is designed to return specified ERC20 tokens to the BackingManager:
function returnTokens(IERC20[] memory erc20s) external notTradingPausedOrFrozen {
// ... (check distribution totals)
for (uint256 i = 0; i < len; ++i) {
require(assetRegistry.isRegistered(erc20s[i]), "unregistered erc20");
erc20s[i].safeTransfer(address(backingManager), erc20s[i].balanceOf(address(this)));
}
}
On the other hand, the manageTokens() function prepares and executes trades based on the current token balances:
function manageTokens(IERC20[] calldata erc20s, TradeKind[] calldata kinds)
external
nonReentrant
notTradingPausedOrFrozen
{
// ... (prepare trades)
for (uint256 i = 0; i < len; ++i) {
IERC20 erc20 = erc20s[i];
if (erc20 == tokenToBuy) continue;
require(address(trades[erc20]) == address(0), "trade open");
require(erc20.balanceOf(address(this)) != 0, "0 balance");
// ... (execute trade)
}
}
The root cause of the issue lies in the lack of synchronization between these two functions. If returnTokens() is called while manageTokens() is in the process of preparing a trade, it can lead to inconsistent state and potential loss of funds. This is because manageTokens() might be operating on outdated balance information if tokens are transferred out by returnTokens() during its execution.
Impact
The race condition between returnTokens() and manageTokens() can lead to failed trades, trades executed with incorrect amounts, or inconsistent contract states. This issue could be exploited by malicious actors to manipulate token balances and potentially cause financial losses to the protocol or its users.
In a worst-case scenario, an attacker could time their transactions to consistently disrupt trading operations, leading to a denial-of-service condition for the trading functionality. This could undermine the core functionality of the RevenueTraderP1 contract and erode user trust in the protocol.
Proof of Concept
Alice calls manageTokens() to initiate a trade for TokenA.
manageTokens() checks the balance of TokenA and starts preparing the trade.
Before the trade is executed, Bob calls returnTokens() for TokenA.
returnTokens() transfers all TokenA back to the BackingManager.
manageTokens() attempts to execute the trade with the now outdated balance information.
The trade fails or executes with an incorrect amount, leading to potential loss of funds or inconsistent state.
Tools Used
Manual review
Recommended Mitigation Steps
To mitigate this issue, implement a locking mechanism to ensure atomic execution of returnTokens() and manageTokens(). This can be achieved by introducing a mutex for each token:
Lines of code
https://github.com/code-423n4/2024-07-reserve/blob/3f133997e186465f4904553b0f8e86ecb7bbacbf/contracts/p1/RevenueTrader.sol#L76-L93 https://github.com/code-423n4/2024-07-reserve/blob/3f133997e186465f4904553b0f8e86ecb7bbacbf/contracts/p1/RevenueTrader.sol#L109-L184
Vulnerability details
Description
The
RevenueTraderP1
contract implements two key functions,returnTokens()
andmanageTokens()
, which are responsible for managing token balances and executing trades. These functions operate on the same set of tokens but lack proper synchronization mechanisms, potentially leading to race conditions and inconsistent contract states.The
returnTokens()
function is designed to return specified ERC20 tokens to theBackingManager
:On the other hand, the
manageTokens()
function prepares and executes trades based on the current token balances:The root cause of the issue lies in the lack of synchronization between these two functions. If
returnTokens()
is called whilemanageTokens()
is in the process of preparing a trade, it can lead to inconsistent state and potential loss of funds. This is becausemanageTokens()
might be operating on outdated balance information if tokens are transferred out byreturnTokens()
during its execution.Impact
The race condition between
returnTokens()
andmanageTokens()
can lead to failed trades, trades executed with incorrect amounts, or inconsistent contract states. This issue could be exploited by malicious actors to manipulate token balances and potentially cause financial losses to the protocol or its users.In a worst-case scenario, an attacker could time their transactions to consistently disrupt trading operations, leading to a denial-of-service condition for the trading functionality. This could undermine the core functionality of the
RevenueTraderP1
contract and erode user trust in the protocol.Proof of Concept
manageTokens()
to initiate a trade for TokenA.manageTokens()
checks the balance of TokenA and starts preparing the trade.returnTokens()
for TokenA.returnTokens()
transfers all TokenA back to theBackingManager
.manageTokens()
attempts to execute the trade with the now outdated balance information.Tools Used
Manual review
Recommended Mitigation Steps
To mitigate this issue, implement a locking mechanism to ensure atomic execution of
returnTokens()
andmanageTokens()
. This can be achieved by introducing a mutex for each token:Assessed type
Other