Users that try to call buyShares and mint shares with only base tokens, will loose those tokens permanently
Summary
When users try to send only base tokens to the GSP contract and try to mint shares with it, they will end up in a situation where their tokens will be lost permanently, as a result of the way the buyShares function is setup
Vulnerability Detail
To Understand How this Vulnerability happens, we need to consider different factors that make it possible
We will be breaking down the buyShares function to understand what is actually happening
function buyShares(address to)
external
nonReentrant
returns (
uint256 shares,
uint256 baseInput,
uint256 quoteInput
)
{
// The balance of baseToken and quoteToken should be the balance minus the fee
uint256 baseBalance = _BASE_TOKEN_.balanceOf(address(this)) - _MT_FEE_BASE_;
uint256 quoteBalance = _QUOTE_TOKEN_.balanceOf(address(this)) - _MT_FEE_QUOTE_;
// The reserve of baseToken and quoteToken
uint256 baseReserve = _BASE_RESERVE_;
uint256 quoteReserve = _QUOTE_RESERVE_;
// The amount of baseToken and quoteToken user transfer to GSP
baseInput = baseBalance - baseReserve;
quoteInput = quoteBalance - quoteReserve;
// BaseToken should be transferred to GSP before calling buyShares
require(baseInput > 0, "NO_BASE_INPUT");
As seen in this first snippet here, when a user sends only base tokens and try to mint we realize that the following is going to happen
The base balance is correctly calculated and the quote will be the same as before
The reserves is gotten, which is a reflection on the current tokens in the pool before this action
The base input on how much to mint for the shares will be correctly calculated but for the quote will be zero
The require statement does not actually revert the transaction, but the condition actually will pass as we stated earlier
Now we will go to the second part to understand how the shares will be minted to the user
else if (baseReserve > 0 && quoteReserve > 0) {
// case 2. normal case
uint256 baseInputRatio = DecimalMath.divFloor(baseInput, baseReserve);
uint256 quoteInputRatio = DecimalMath.divFloor(quoteInput, quoteReserve);
uint256 mintRatio = quoteInputRatio < baseInputRatio ? quoteInputRatio : baseInputRatio;
// The shares will be minted to user
shares = DecimalMath.mulFloor(totalSupply, mintRatio);
// The target will be updated
_BASE_TARGET_ = uint112(uint256(_BASE_TARGET_) + (DecimalMath.mulFloor(uint256(_BASE_TARGET_), mintRatio)));
_QUOTE_TARGET_ = uint112(uint256(_QUOTE_TARGET_) + (DecimalMath.mulFloor(uint256(_QUOTE_TARGET_), mintRatio)));
}
Because quote input will be zero and this function uses the ratio as a means of calculating the shares to be distributed to the user as shown in that snippet above, the shares to be minted to be zero hereby the user will not get anything for their buyShares call and can go and fix their mistake and deposit the quote so as to mint correctly right ?, but the function does not work like that it assumes some shares as been minted already and goes ahead to perform the remaining actions.
We will Now Explore the last snippets that will highlight the loss of tokens for the user
// The shares will be minted to user
// The reserve will be updated
_mint(to, shares);
_setReserve(baseBalance, quoteBalance);
Here We can see that a setReserves function is called, to update the amount of reserves currently in the pool, when we check the function, we will see how the vulnerability happens
function _setReserve(uint256 baseReserve, uint256 quoteReserve) internal {
// the reserves should be less than the max uint112
require(baseReserve <= type(uint112).max && quoteReserve <= type(uint112).max, "OVERFLOW");
_BASE_RESERVE_ = uint112(baseReserve);
_QUOTE_RESERVE_ = uint112(quoteReserve);
// if _IS_OPEN_TWAP_ is true, update the twap price
if (_IS_OPEN_TWAP_) _twapUpdate();
}
Here we can see that now the reserves will now be updated to the balance, which inculdes the user previously deposited tokens, keeping an up to date count
But when we go back to the start and see how the base input is calculated,
// The amount of baseToken and quoteToken user transfer to GSP
baseInput = baseBalance - baseReserve;
quoteInput = quoteBalance - quoteReserve;
We see that if the User wanted to rectify their mint issue, they would essentially have zero base input and will be unable to retrieve those lost tokens permanently
Impact
users will loose their tokens permanently if they deposited only base token and try to mint shares with it
Code Snippet
Tool used
Manual Review
Recommendation
Simple recommendation here is that in that require statement a check for both tokens should be implemented
// BaseToken should be transferred to GSP before calling buyShares
require(baseInput > 0 && quoteInput > 0, "NO_INPUT");
This would constitute user input error, since they are not supposed to add liquidity on just one side given buyShares are intended to be called via the proxy here
inzinko
high
Users that try to call
buyShares
and mint shares with only base tokens, will loose those tokens permanentlySummary
When users try to send only base tokens to the
GSP
contract and try to mint shares with it, they will end up in a situation where their tokens will be lost permanently, as a result of the way thebuyShares
function is setupVulnerability Detail
To Understand How this Vulnerability happens, we need to consider different factors that make it possible
We will be breaking down the
buyShares
function to understand what is actually happeninghttps://github.com/sherlock-audit/2023-12-dodo-gsp/blob/main/dodo-gassaving-pool/contracts/GasSavingPool/impl/GSPFunding.sol#L31C5-L82C6
As seen in this first snippet here, when a user sends only base tokens and try to mint we realize that the following is going to happen
Now we will go to the second part to understand how the shares will be minted to the user
Because quote input will be zero and this function uses the ratio as a means of calculating the shares to be distributed to the user as shown in that snippet above, the shares to be minted to be zero hereby the user will not get anything for their
buyShares
call and can go and fix their mistake and deposit the quote so as to mint correctly right ?, but the function does not work like that it assumes some shares as been minted already and goes ahead to perform the remaining actions.We will Now Explore the last snippets that will highlight the loss of tokens for the user
Here We can see that a
setReserves
function is called, to update the amount of reserves currently in the pool, when we check the function, we will see how the vulnerability happenshttps://github.com/sherlock-audit/2023-12-dodo-gsp/blob/main/dodo-gassaving-pool/contracts/GasSavingPool/impl/GSPVault.sol#L105C5-L112C6
Here we can see that now the reserves will now be updated to the balance, which inculdes the user previously deposited tokens, keeping an up to date count
But when we go back to the start and see how the base input is calculated,
We see that if the User wanted to rectify their mint issue, they would essentially have zero base input and will be unable to retrieve those lost tokens permanently
Impact
users will loose their tokens permanently if they deposited only base token and try to mint shares with it
Code Snippet
Tool used
Manual Review
Recommendation
Simple recommendation here is that in that require statement a check for both tokens should be implemented
Duplicate of #49