gm() season transition can lead to early germination and twa reserves manipulation
Report ID
#37870
Report type
Smart Contract
Has PoC?
Yes
Target
https://arbiscan.io/address/0xD1A0060ba708BC4BCD3DA6C37EFa8deDF015FB70
Impacts
- Invariant is missing on a function where it should be implemented
- Illegitimate minting of protocol native assets
Description
Summary
After a period of inactivity, the gm() function allows two season advancements in rapid succession. This vulnerability can lead to two primary issues:
- Premature germination of deposits, potentially allowing users to bypass intended waiting periods.
- Manipulation of Time-Weighted Average (TWA) reserves and deltaB calculations, which could result in illegitimate minting of protocol native assets (Beans).
Vulnerability details
1. SeasonFacet.sol
function _gm(address account, LibTransfer.To mode) private returns (uint256) {
require(!s.sys.paused, "Season: Paused.");
require(seasonTime() > s.sys.season.current, "Season: Still current Season.");
checkSeasonTime();
uint32 season = stepSeason();
int256 deltaB = stepOracle();
// ... (other operations)
}This function, _gm, is the core of the season transition mechanism. It's called when advancing to a new season. The function first checks if the system is paused and if it's time for a new season. The checkSeasonTime() function is then called, which is where the vulnerability primarily lies.
function checkSeasonTime() internal {
uint32 _seasonTime = seasonTime();
uint32 currentSeason = s.sys.season.current;
require(_seasonTime > currentSeason, "Season: Still current Season.");
if (_seasonTime > currentSeason + 1) {
s.sys.season.start += s.sys.season.period.mul(_seasonTime - currentSeason - 1);
}
}The checkSeasonTime() function is intended to prevent multiple season advancements in quick succession. However, when _seasonTime > currentSeason + 1, which can occur after a period of inactivity (a "stale period"), the function updates s.sys.season.start to account for the missed seasons. This update allows the next gm call to proceed immediately, even if it's called just seconds later. The reason for that is s.sys.season.start can become equal to block.timestamp - period - 1, meaning at 1 second later the gm function can be called again, as can be seen at the Proof of Concept.
With two gm calls made in rapid succession, a couple of side-effects can happen at the system.
2. LibWellMinting.sol
function capture(address well) external returns (int256 deltaB) {
bytes memory lastSnapshot = LibAppStorage.diamondStorage().sys.wellOracleSnapshots[well];
if (lastSnapshot.length > 0) {
deltaB = updateOracle(well, lastSnapshot);
} else {
initializeOracle(well);
}
deltaB = LibMinting.checkForMaxDeltaB(deltaB);
}The capture function is crucial for updating the oracle and calculating deltaB, which is used in minting calculations. When gm is called, stepOracle() invokes this capture function.
The vulnerability in SeasonFacet.sol allows this function to be called twice in quick succession. This rapid calling can lead to inaccurate deltaB calculations because the time between snapshots becomes very small, opening up the possibility for artificial-demand to be taken into account.
This inaccuracy can be exploited to manipulate the minting process, especially because with a lookback value of a few seconds, the twa system will account for reserves of the past seconds - as can be seen at `LibUniswapOracle.consult:
3. LibSilo.sol
function _depositAndGerminate(
address account,
address token,
uint256 amount,
uint256 bdv
) internal returns (uint256 stem) {
stem = LibGerminate.getGerminatingStem(token);
_depositAndGerminateTo(account, token, amount, bdv, stem);
}This function handles the deposit and germination process for tokens in the Silo. The vulnerability in season advancement interacts with this process in several ways:
- The getGerminatingStem function likely relies on the current season or some time-based calculation to determine the appropriate stem for germination.
- Rapid season advancements can cause this function to return stems that are much further in the future than intended.
- This can lead to deposits germinating (becoming active and eligible for rewards) much earlier than they should.
The premature germination of deposits disrupts the intended economic model of the protocol. It can give unfair advantages to users who can time their deposits just before a rapid season advancement, allowing them to bypass the intended waiting period and potentially claim rewards or voting rights earlier than designed.
Impact
The vulnerability in the gm() function allows rapid season transitions that can lead to illegitimate minting of protocol native assets by allowing the manipulation of deltaB and early germinations.
Proof of concept
The provided foundry Proof of Concept demonstrates the vulnerability:
The tests can be run by first initializing a foundry project and installing Oz's library with the following commands:
forge init
forge install OpenZeppelin/openzeppelin-contracts --no-commit
forge test --match-test testEarlyDepositConversion -vvThe doubleGmCall function simulates a scenario where:
- The system has been inactive for about 5 hours (simulating a stale period).
- A user calls gm once, advancing the season.
- Just one second later, the user calls gm again, successfully advancing the season a second time.
This demonstrates that after a period of inactivity, the system allows multiple season advancements in rapid succession, which should not be possible under normal circumstances. Since the second gm call was made exactly 1 second after the first gm call, it is also proven that the lookback value for the oracles will be of 1 second.
The testEarlyDepositConversion function further illustrates the potential impact:
- It simulates a deposit before the rapid season advancements.
- After the double gm call, it attempts to convert the deposit.
- While the conversion fails due to an oracle issue, the fact that the system allows the attempt demonstrates that the deposit has prematurely reached a state where conversion should be possible.
This proof of concept shows how there is a vulnerability that can be exploited to manipulate the protocol's time-sensitive mechanisms.
BIC Response
Thank you for your report. While these findings are correct, this is already a known issue that has come up in past audits. You can find the public report here: https://codehawks.cyfrin.io/c/2024-05-beanstalk-the-finale/results?lt=contest&page=1&sc=reward&sj=reward&t=report