Platform: Code4rena
Start Date: 30/05/2023
Pot Size: $300,500 USDC
Total HM: 79
Participants: 101
Period: about 1 month
Judge: Trust
Total Solo HM: 36
Id: 242
League: ETH
Rank: 48/101
Findings: 2
Award: $450.98
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0xTheC0der
Also found by: Atree, BLOS, BPZ, Fulum, KupiaSec, SpicyMeatball, bin2chen, jasonxiale, lsaudit, minhquanym, xuwinnie, zzzitron
95.3782 USDC - $95.38
The asset removal logic would be broken as it tracks the wrong assetId
after a removal.
The owner can remove an asset using removeAsset()
and it updates the assetId
mapping inside the function.
function removeAsset(address asset) external nonReentrant onlyOwner { // No need to check if index is 0, it will underflow and revert if it is 0 uint256 assetIndex = assetId[asset] - 1; uint256 newAssetsLength = assets.length - 1; if (newAssetsLength == 0) revert CannotRemoveLastAsset(); totalWeights -= weights[assetIndex]; address lastAsset = assets[newAssetsLength]; assetId[lastAsset] = assetIndex; //@audit should be assetIndex + 1 assets[assetIndex] = lastAsset; weights[assetIndex] = weights[newAssetsLength]; assets.pop(); weights.pop(); assetId[asset] = 0; emit AssetRemoved(asset); updateAssetBalances(); asset.safeTransfer(msg.sender, asset.balanceOf(address(this))); }
In this protocol, valid asset ids are started from 1 but it sets the raw assetIndex
after the removal. After that, the asset removals wouldn't work properly.
assets = [A, B, C], assetId[A] = 1, assetId[B] = 2, assetId[C] = 3
.removeAsset(A)
.assets = [B, C], assetId[B] = 2, assetId[C] = 0
because assetIndex == 0
inside the function.C
asset can't be removed because assetId[C] = 0
and it will be considered as an unadded asset.Manual Review
removeAsset() should be modified like this.
assetId[lastAsset] = assetIndex + 1;
Other
#0 - c4-judge
2023-07-09T16:28:47Z
trust1995 marked the issue as duplicate of #703
#1 - c4-judge
2023-07-09T16:28:51Z
trust1995 marked the issue as satisfactory
#2 - c4-judge
2023-07-11T17:20:49Z
trust1995 changed the severity to 3 (High Risk)
🌟 Selected for report: 0xTheC0der
355.6046 USDC - $355.60
https://github.com/code-423n4/2023-05-maia/blob/d0ebf632cf8caddb21e39d49ae15b18245319cd2/src/erc-4626/ERC4626MultiToken.sol#L100 https://github.com/code-423n4/2023-05-maia/blob/d0ebf632cf8caddb21e39d49ae15b18245319cd2/src/erc-4626/ERC4626MultiToken.sol#L138
Users might lose assets when they use deposit()
or withdraw()
.
This multitoken works with several assets that have different weights and users can mint shares by depositing assets.
function deposit(uint256[] calldata assetsAmounts, address receiver) public virtual nonReentrant returns (uint256 shares) { // Check for rounding error since we round down in previewDeposit. require((shares = previewDeposit(assetsAmounts)) != 0, "ZERO_SHARES"); //@audit possible loss of assets // Need to transfer before minting or ERC777s could reenter. receiveAssets(assetsAmounts); _mint(receiver, shares); emit Deposit(msg.sender, receiver, assetsAmounts, shares); afterDeposit(assetsAmounts, shares); }
When users call deposit()
with a list of assets, it calculates the shares inside convertToShares()
.
function convertToShares(uint256[] calldata assetsAmounts) public view virtual returns (uint256 shares) { uint256 _totalWeights = totalWeights; uint256 length = assetsAmounts.length; if (length != assets.length) revert InvalidLength(); shares = type(uint256).max; for (uint256 i = 0; i < length;) { uint256 share = assetsAmounts[i].mulDiv(_totalWeights, weights[i]); if (share < shares) shares = share; unchecked { i++; } } }
convertToShares()
returns the minimum share among all shares for assets. But the asset weights might be changed by the owner and users might lose funds unexpectedly.
A
and B
. weights[A] = 1, weights[B] = 1, totalWeights = 2
deposit()
with 50 amounts of A
and B
.weights[A] = 2, weights[B] = 1, totalWeights = 3
.A
will be 50 * 3 / 2 = 75
. 50 * 3 / 1 = 150
for B
. So Alice will get min(75, 150) = 75 shares
instead of 100.50A
and 25B
and it means Alice loses 25B
.Similarly, users might burn more shares than they expected after asset weights are changed with withdraw()
.
Manual Review
I think we should add a refund logic to deposit()/withdraw()
functions so users can get back the unused assets.
ERC4626
#0 - c4-judge
2023-07-10T10:14:18Z
trust1995 marked the issue as duplicate of #281
#1 - c4-judge
2023-07-10T10:14:25Z
trust1995 marked the issue as satisfactory
#2 - c4-judge
2023-07-25T14:04:42Z
trust1995 marked the issue as unsatisfactory: Invalid
#3 - c4-judge
2023-07-28T11:57:19Z
trust1995 marked the issue as satisfactory