The issue with this approach is that if token to be removed is an ERC20Upgradeable, it would be possible to change the internal token transfer function to always revert after it was added to the vault, which would cause UlyssesToken.addAsset() and UlyssesToken.removeAsset() to always revert. UlyssesToken.removeAsset() will revert on L84 and UlyssesToken.addAsset() will revert since it will call UlyssesToken.updateAssetBalances().
admin adds a token by calling UlyssesToken.addAsset(), assume the token was created using ERC20Upgradeable and the token is working normally when it's being added initially
some time later, the token gets compromised - or the token creators decide to grief Maia multi token vaults - and update the token contract to always revert when calling transfer() and transferFrom()
the admin tries to remove the token by calling UlyssesToken.removeAsset(), but L84 will revert
since the asset cannot be removed, the admin won't be able to call UlyssesToken.addAsset() as well, because UlyssesToken.updateAssetBalances() will call the transfer function from the malicious ERC20
Note that the safe transfer variant will handle tokens that don't return a value, but it will still revert if the function being called reverts.
Impact
If a malicious ERC20 gets added into the vault, no asset will be able to be added or removed from UlyssesToken/ERC4626MultiToken, every call to UlyssesToken.addAsset() and UlyssesToken.removeAsset() will revert.
Note a malicious token cannot be added directly because the UlyssesToken.addAsset() - UlyssesToken.updateAssetBalances() would revert. Therefore, for this exploit to happen, it would have to be a ERC20Upgradeable that gets updated after it gets added to the vault.
Consider removing L84 and save the value to be transferred in a mapping with the asset address as the key and the amount as the value, and create a new function to execute the transfer.
With this approach, on UlyssesToken.removeAsset() it will be possible to remove ERC20 tokens even if the transfer function for the token to be removed reverts.
Lines of code
https://github.com/code-423n4/2023-05-maia/blob/main/src/ulysses-amm/UlyssesToken.sol#L60-L85
Vulnerability details
Proof of Concept
UlyssesToken.removeAsset()
will make an ERC20 transfer to send the removed asset funds fromUlyssesToken
to the caller.https://github.com/code-423n4/2023-05-maia/blob/main/src/ulysses-amm/UlyssesToken.sol#L60-L85
The issue with this approach is that if token to be removed is an ERC20Upgradeable, it would be possible to change the internal token transfer function to always revert after it was added to the vault, which would cause
UlyssesToken.addAsset()
andUlyssesToken.removeAsset()
to always revert.UlyssesToken.removeAsset()
will revert on L84 andUlyssesToken.addAsset()
will revert since it will callUlyssesToken.updateAssetBalances()
.https://github.com/code-423n4/2023-05-maia/blob/main/src/ulysses-amm/UlyssesToken.sol#L115-L119
Following scenario could happen:
UlyssesToken.addAsset()
, assume the token was created using ERC20Upgradeable and the token is working normally when it's being added initiallytransfer()
andtransferFrom()
UlyssesToken.removeAsset()
, but L84 will revertUlyssesToken.addAsset()
as well, becauseUlyssesToken.updateAssetBalances()
will call the transfer function from the malicious ERC20Note that the safe transfer variant will handle tokens that don't return a value, but it will still revert if the function being called reverts.
Impact
If a malicious ERC20 gets added into the vault, no asset will be able to be added or removed from
UlyssesToken/ERC4626MultiToken
, every call toUlyssesToken.addAsset()
andUlyssesToken.removeAsset()
will revert.Note a malicious token cannot be added directly because the
UlyssesToken.addAsset() - UlyssesToken.updateAssetBalances()
would revert. Therefore, for this exploit to happen, it would have to be a ERC20Upgradeable that gets updated after it gets added to the vault.Tools Used
Manual review.
Recommended Mitigation Steps
In
UlyssesToken.removeAsset()
, favor pull over push, see withdrawal from contracts pattern.Consider removing L84 and save the value to be transferred in a mapping with the asset address as the key and the amount as the value, and create a new function to execute the transfer.
With this approach, on
UlyssesToken.removeAsset()
it will be possible to remove ERC20 tokens even if the transfer function for the token to be removed reverts.Assessed type
ERC20