If some SOP rewards during a flood are allocated to a stalkholder, and the stalkholder does not claim their rewards, and later the well is de-whitelisted by the authorized caller for any reason, the stalkholder cannot claim their reward anymore, and the reward will be locked in the protocol.
The root cause of this issue is that when a token is de-whitelisted by the authorized caller, there is no check for any nonzero claimable rewards. Additionally, it does not require stalkholders (who are entitled to some rewards) to claim their rewards.
When the user claims their reward, the system checks whether the well is whitelisted by calling the function isWell(address)
.
Please note that the possibility of such a scenario is not low, because users often do not claim their rewards immediately. Therefore, it is highly likely that there will always be nonzero claimable rewards for a well that is going to be de-whitelisted.
In the following test, the user deposits and mows in seasons 5 and 6, respectively. Then, there is rain and a flood in seasons 7 and 8, respectively. The user mows to see the amount of rewards (balanceOfPlenty) allocated to him. It shows that the allocated reward is 51191151829696906017
SOP tokens. However, before the user claims his reward, the owner de-whitelists the well. So, when the user calls claimPlenty
, he receives no rewards.
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");
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, mockBeanstalk] = await getAllBeanstalkContracts(this.diamond.address);
bean = await ethers.getContractAt("Bean", BEAN);
this.weth = await ethers.getContractAt("MockToken", WETH);
mockBeanstalk.deployStemsUpgrade();
await mockBeanstalk.siloSunrise(0);
await bean.connect(user).approve(beanstalk.address, "100000000000");
await bean.connect(user2).approve(beanstalk.address, "100000000000");
await bean.mint(user.address, to6("10000"));
await bean.mint(user2.address, to6("10000"));
[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"));
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 mockBeanstalk.siloSunrise(0);
await mockBeanstalk.captureWellE(this.well.address);
await setEthUsdChainlinkPrice("1000");
await setStethEthChainlinkPrice("1000");
await setStethEthChainlinkPrice("1");
await setWstethEthUniswapPrice("1");
await this.well.setReserves([to6("1000000"), to18("1100")]);
await this.pump.setInstantaneousReserves(this.well.address, [to6("1000000"), to18("1100")]);
await mockBeanstalk.siloSunrise(0);
await mockBeanstalk.siloSunrise(0);
});
beforeEach(async function () {
snapshotId = await takeSnapshot();
});
afterEach(async function () {
await revertToSnapshot(snapshotId);
});
describe("consecutive floods scenarios", async function () {
it("dewhitelisted well disallows claiming rewards", async function () {
console.log("current season: ", await beanstalk.season());
await beanstalk.connect(user).deposit(bean.address, to6("1000"), EXTERNAL);
await mockBeanstalk.siloSunrise(0);
await beanstalk.mow(user.address, bean.address);
await mockBeanstalk.rainSunrise();
await mockBeanstalk.rainSunrise();
await beanstalk.mow(user.address, bean.address);
const balanceOfPlentyUser1 = await beanstalk.balanceOfPlenty(user.address, this.well.address);
console.log("user1's balanceOfPlenty: ", balanceOfPlentyUser1);
await beanstalk.connect(owner).dewhitelistToken(this.well.address);
await beanstalk.connect(user).claimPlenty(this.well.address, EXTERNAL);
console.log("balance user1: ", await this.weth.balanceOf(user.address));
});
})
Lock of rewards if the well is de-whitelisted.
It is recommended that during the de-whitelisting of a well, it should be ensured that there are no claimable rewards on that well. However, this solution can introduce another attack vector where an attacker intentionally does not claim their reward to prevent the well from being de-whitelisted.
Another solution is that when claiming rewards, it checks the well is whitelisted now or it was whitelisted before.