`_setReserves()` fails when supporting Well implementation with an odd number of tokens
Report ID
#34357
Report type
Smart Contract
Has PoC?
Yes
Target
https://etherscan.io/address/0xBA510e11eEb387fad877812108a3406CA3f43a4B
Impacts
- Permanent freezing of funds
- Contract fails to deliver promised returns, but doesn't lose value
Description
In a Well.sol implementation with an odd number of tokens, _setReserves() will always revert after the first successful call, due to faulty implementation of the LibBytes.storeUint128() function.
Root Cause
The _setReserves() function is used to both verify and store the token balances of the Well.sol deployment for all external functions (i.e. swapTo(), swapFrom(), ...). It does so by first looping through all of the tokens, and comparing the stored token balance (in reserves) with the actual token balance (obtained by balanceOf()). It does not cause any issues when there are two tokens supported by the Well.sol implementation, which is how it is currently deployed.
However, in a Well.sol deployment that supports an odd number of tokens (I.e. 3) there is
a permanent skewing of the internal contract balance stored in reserves and the actual token balance of the contract obtained by balanceOf(). It is caused by a faulty implementation of the _setReserves() functionality; more specifically, the LibBytes.storeUint128() function. The memory storage implemented by LibBytes.storeUint128() does not properly function when the Well.sol implementation contains and odd number of tokens, and the wrong token value is copied and saved for the final token.
Impact
- Most external functions will always revert when called
The main impact of the vulnerability is that in a Well.sol deployment with an odd number of tokens, no function will be able to execute successfully, and will always revert. This is because most external functions available for users to call all contain the _setReserves() function to verify the token balance. Because of the incorrect storage, _setReserves() will always revert when verifying the stored token balance against the actual balance.
POC
The POC was created in the [Github directory](https://github.com/BeanstalkFarms/Basin/tree/master), as currently, a Well.sol deployment with an odd number of tokens does not exist. Specifically, it is designed as a forge test in the /test directory.
Here are the steps performed in the POC:
- First, a
Well.soldeployment with 3 tokens is initialized. - Then, a simple swap occurs between the first two tokens, with the amount out being 123*10^18.
- Then, a second swap is attempted. However, it will revert for the reasons mentioned above.
For the code, see the POC section. To run the POC, create a test in the test directory (i.e. {test_name}.t.sol) and run the command forge test -vv --match-path test/{test_name}.t.sol from the main directory.
To see the logs mentioned in the Expected Results section, uncomment the piece of code that says: "Uncomment to see logs".
Expected Results
The results of the POC are shown below. There are two different results: one with the logs enabled, and one without.
No Logs:
[â ’] Compiling...
[â °] Compiling 1 files with Solc 0.8.20
[â ”] Solc 0.8.20 finished in 7.28s
Compiler run successful!
Ran 1 test for test/Well.POC.t.sol:WellPOC
[PASS] test_POC(uint256) (runs: 257, μ: 184963, ~: 184963)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 171.48ms (158.01ms CPU time)
Ran 1 test suite in 181.07ms (171.48ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)With Logs Enabled:
Explanation
The second swap will fail due to an InvalidReserves() revert, as demonstrated by the expectRevert() check in the POC. This is because the internal balances of the contract (in reserves) does not match the actual balances in the contract (obtained by balanceOf()). In particular, the former is logged as the Well Token 1 Balance, Well Token 2 Balance, and Well Token 3 Balance, while the latter is logged in the lines beginning with [i] Before Swap 2: Well Reserve. We can see that the balance of Token 2 does not match after the swap, causing the revert.
Recommended Mitigation
Modify the padding applied to the token reserve when there is an odd number of tokens. More specifically, the code on line 54 of LibBytes.sol:
... add(mload(add(reserves, add(iByte, 32))), shr(128, shl(128, sload(add(slot, maxI)))))Using a simple padding with the first reserve like so:
... add(mload(add(reserves, add(iByte, 32))), shr(128, shl(128, mload(add(reserves, 0)))))resolves the vulnerability.
References
Proof of concept
This POC was created for the Basin Github Directory. To run the POC, create a test in the test directory (i.e. {test_name}.t.sol) and run the command forge test -vv --match-path test/{test_name}.t.sol from the main directory.
BIR-19: Odd Token Wells
BIC Response
After some investigation we have confirmed this is a valid bug report. The issue has been resolved in our codebase here https://github.com/BeanstalkFarms/Basin/pull/136/files
As outlined in the program, for Medium severity reports, the BIC determines a reward between 1k and 10k Beans based on:
The exploitability of the bug; The impact it causes; and The likelihood of the vulnerability presenting itself.
Given that this issue is not exploitable, and would likely be caught in preliminary testing before any 3+ token Well were deployed on-chain, the BIC has determined this report to be awarded 3,000 Beans.