Summary
The function to initialize the whitelisted tokens on L2 will revert due to an out of bound error because when the whitelistedStatuses
array have 0 elements the loop will try to access the 0 position and since it will not exist, it will revert
Relevant GitHub Links:
https://github.com/Cyfrin/2024-05-beanstalk-the-finale/blob/main/protocol/contracts/libraries/Silo/LibWhitelistedTokens.sol#L296-L318
Vulnerability Details
When the protocol will be deployed on L2 will follow the next sequence:
reseeds = [
reseed1,
reseedDeployL2Beanstalk,
reseed3,
reseed4,
reseed5,
reseed6,
reseed7,
reseed8,
reseed9
];
As we can see, the diamond on L2 will not execute any init function, all the reseeds should migrate succesfully all the state from the L1.
However, let's see how it reseeds the whitelisted tokens.
function init(address[] calldata tokens, AssetSettings[] calldata asset) external {
for (uint i; i < tokens.length; i++) {
LibWhitelist.whitelistToken(
tokens[i],
asset[i].selector,
asset[i].stalkIssuedPerBdv,
asset[i].stalkEarnedPerSeason,
asset[i].encodeType,
asset[i].gaugePointImplementation.selector,
asset[i].liquidityWeightImplementation.selector,
asset[i].gaugePoints,
asset[i].optimalPercentDepositedBdv,
asset[i].oracleImplementation
);
}
}
To whitelist the tokens, the function whitelistToken
will be be called. Inside there, the main logic is executed from the LibWhitelist::whitelistToken
. This function does the following:
function whitelistToken(
address token,
bytes4 selector,
uint32 stalkIssuedPerBdv,
uint32 stalkEarnedPerSeason,
bytes1 encodeType,
bytes4 gaugePointSelector,
bytes4 liquidityWeightSelector,
uint128 gaugePoints,
uint64 optimalPercentDepositedBdv,
Implementation memory oracleImplementation
) internal {
...
verifyWhitelistStatus(token, selector, stalkIssuedPerBdv);
...
}
function verifyWhitelistStatus(
address token,
bytes4 selector,
uint32 stalkIssuedPerBdv
) internal {
(bool isWhitelisted, bool previouslyWhitelisted) = LibWhitelistedTokens.checkWhitelisted(
token
);
...
}
It checks if the token that is wanted to be whitelisted is already in the list to prevent a copy of it. This check is coded in the LibWhitelistedTokens.checkWhitelisted
:
function checkWhitelisted(
address token
) internal view returns (bool isWhitelisted, bool previouslyWhitelisted) {
AppStorage storage s = LibAppStorage.diamondStorage();
uint256 whitelistedStatusLength = s.sys.silo.whitelistStatuses.length;
uint256 i;
while (s.sys.silo.whitelistStatuses[i].token != token) {
i++;
if (i >= whitelistedStatusLength) {
return (false, false);
}
}
if (s.sys.silo.whitelistStatuses[i].isWhitelisted) {
return (true, false);
} else {
return (false, true);
}
}
This function is intended to loop through all the whitelisted tokens in order to find or not the token in the array. However, if the list is empty, it will try to access the first element and it will revert due to out of bound.
Since there will be no initialization of the diamond before the reseeds on L2, the init function to whitelist the tokens will revert due to this error because the array of whitelisted tokens will have 0 elements and the first condition in the while loop will try to access the array s.sys.silo.whitelistStatuses[i].token
i being 0 and since it will be empty it will revert.
Impact
Medium, the reseed function will revert but no funds are at risk
Tools Used
Manual review
Recommendations
Add the following changes:
function checkWhitelisted(
address token
) internal view returns (bool isWhitelisted, bool previouslyWhitelisted) {
AppStorage storage s = LibAppStorage.diamondStorage();
uint256 whitelistedStatusLength = s.sys.silo.whitelistStatuses.length;
+ if(whitelistedStatusLength == 0){
+ return (false, false);
+ }
uint256 i;
while (s.sys.silo.whitelistStatuses[i].token != token) {
i++;
if (i >= whitelistedStatusLength) {
return (false, false);
}
}
if (s.sys.silo.whitelistStatuses[i].isWhitelisted) {
return (true, false);
} else {
return (false, true);
}
}