function rescueTokens(address tokenToRescue, address to, uint256 amount) external virtual onlyOwner nonReentrant {
if (tokenToRescue == lpToken) {
require(amount <= IERC20(lpToken).balanceOf(address(this)).sub(_totalStakeLpToken),
"MuteAmplifier::rescueTokens: that Token-Eth belongs to stakers"
);
} else if (tokenToRescue == muteToken) {
if (totalStakers > 0) {
require(amount <= IERC20(muteToken).balanceOf(address(this)).sub(totalRewards.sub(totalClaimedRewards)),
"MuteAmplifier::rescueTokens: that muteToken belongs to stakers"
);
}
}
IERC20(tokenToRescue).transfer(to, amount);
}
You can see that lpToken and muteToken cannot be transferred unless there is an excess amount beyond what is needed by the contract.
So stakers can be sure that not even the contract owner can mess with their stakes.
The issue is that lpToken and muteToken are not the only tokens that need to stay in the contract.
There is also the fee0 token and the fee1 token.
So what can happen is that the owner can withdraw fee0 or fee1 tokens and users cannot payout rewards or withdraw their stake because the transfer of fee0 / fee1 tokens reverts due to the missing balance.
Users can of course send fee0 / fee1 tokens to the contract to restore the balance. But this is not intended and certainly leaves the user worse off.
Proof of Concept
Assume that when an update occurs via the update modifier there is an amount of fee0 tokens claimed:
We can see that _totalWeightFee0 is updated such that when a user's rewards are calculated the fee0 tokens will be paid out to the user.
What happens now is that the owner calls rescueTokens which transfers the fee0 tokens out of the contract.
We can see that when the fee0 to be paid out to the user is calculated in the _applyReward function, the calculation is solely based on _totalWeightFee0 and does not take into account if the fee0 tokens still exist in the contract.
So when the fee0 tokens are attempted to be transferred to the user that calls payout or withdraw, the transfer reverts due to insufficient balance.
Tools Used
VSCode
Recommended Mitigation Steps
The MuteAmplifier.rescueTokens function should check that only excess fee0 / fee1 tokens can be paid out. Such that tokens that will be paid out to stakers need to stay in the contract.
Fix:
diff --git a/contracts/amplifier/MuteAmplifier.sol b/contracts/amplifier/MuteAmplifier.sol
index 9c6fcb5..b154d81 100644
--- a/contracts/amplifier/MuteAmplifier.sol
+++ b/contracts/amplifier/MuteAmplifier.sol
@@ -188,6 +188,18 @@ contract MuteAmplifier is Ownable{
"MuteAmplifier::rescueTokens: that muteToken belongs to stakers"
);
}
+ } else if (tokenToRescue == address(IMuteSwitchPairDynamic(lpToken).token0())) {
+ if (totalStakers > 0) {
+ require(amount <= IERC20(IMuteSwitchPairDynamic(lpToken).token0()).balanceOf(address(this)).sub(totalFees0.sub(totalClaimedFees0)),
+ "MuteAmplifier::rescueTokens: that token belongs to stakers"
+ );
+ }
+ } else if (tokenToRescue == address(IMuteSwitchPairDynamic(lpToken).token1())) {
+ if (totalStakers > 0) {
+ require(amount <= IERC20(IMuteSwitchPairDynamic(lpToken).token1()).balanceOf(address(this)).sub(totalFees1.sub(totalClaimedFees1)),
+ "MuteAmplifier::rescueTokens: that token belongs to stakers"
+ );
+ }
}
IERC20(tokenToRescue).transfer(to, amount);
The issue discussed in this report also ties in with the fact that the fee0 <= totalFees0 && fee1 <= totalFees1 check before transferring fee tokens always passes. It does not prevent the scenario that the sponsor wants to prevent which is when there are not enough fee tokens to be transferred the transfer should not block the function.
So in addition to the above changes I propose to add these changes as well:
Lines of code
https://github.com/code-423n4/2023-03-mute/blob/4d8b13add2907b17ac14627cfa04e0c3cc9a2bed/contracts/amplifier/MuteAmplifier.sol#L180-L194
Vulnerability details
Impact
The
MuteAmplifier.rescueTokens
function allows theowner
to withdraw tokens that are not meant to be in this contract.The contract does protect tokens that ARE meant to be in the contract by not allowing them to be transferred:
Link
You can see that
lpToken
andmuteToken
cannot be transferred unless there is an excess amount beyond what is needed by the contract.So stakers can be sure that not even the contract
owner
can mess with their stakes.The issue is that
lpToken
andmuteToken
are not the only tokens that need to stay in the contract.There is also the
fee0
token and thefee1
token.So what can happen is that the
owner
can withdrawfee0
orfee1
tokens and users cannotpayout
rewards orwithdraw
their stake because the transfer offee0
/fee1
tokens reverts due to the missing balance.Users can of course send
fee0
/fee1
tokens to the contract to restore the balance. But this is not intended and certainly leaves the user worse off.Proof of Concept
Assume that when an update occurs via the
update
modifier there is an amount offee0
tokens claimed:Link
We can see that
_totalWeightFee0
is updated such that when a user's rewards are calculated thefee0
tokens will be paid out to the user.What happens now is that the
owner
callsrescueTokens
which transfers thefee0
tokens out of the contract.We can see that when the
fee0
to be paid out to the user is calculated in the_applyReward
function, the calculation is solely based on_totalWeightFee0
and does not take into account if thefee0
tokens still exist in the contract.Link
So when the
fee0
tokens are attempted to be transferred to the user that callspayout
orwithdraw
, the transfer reverts due to insufficient balance.Tools Used
VSCode
Recommended Mitigation Steps
The
MuteAmplifier.rescueTokens
function should check that only excessfee0
/fee1
tokens can be paid out. Such that tokens that will be paid out to stakers need to stay in the contract.Fix:
The issue discussed in this report also ties in with the fact that the
fee0 <= totalFees0 && fee1 <= totalFees1
check before transferring fee tokens always passes. It does not prevent the scenario that the sponsor wants to prevent which is when there are not enough fee tokens to be transferred the transfer should not block the function.So in addition to the above changes I propose to add these changes as well: