Open code423n4 opened 2 years ago
Contract checks if you own it as owner of Basket has bought it, and as such is entitled to underlying tokens
The attack is contingent on a regular user, creating a smart contract, which allows anybody to call it, which checks that a parameter-supplied Nibbl vault contains a Nibbl basket which contains a specific NFT, and then proceeds to buyout/buy shares of that vault.
Honestly it seems like the vector of attack is possible but quite far fetched.
Creative thinking, this is what I'm here for!
If I'm following the flow correctly.. you kick off a bulk withdraw from the basket, the first NFT in the list is a malicious contract which then creates a vault for that basket (so the contract needs to be the basket owner, which is okay). Now the vault is fully created for that basket which still has valuable NFTs in it but you're mid-tx. Your malicious contract pings other contracts which can be prompted to ape in via on-chain logic -- their logic confirms all looks well and buys. But then control returns to the original batch withdrawal and the basket is drained.
Honestly it seems like the vector of attack is possible but quite far fetched.
I think I'd agree. To put it in terms of risk, this is not High: "valid attack path that does not have hand-wavy hypotheticals" -- this sounds a bit hand-wavy. Namely because it assumes Invest_Contract
allows any address to trigger a purchase using other users funds which seems risky. And the victim here is Invest_Contract
, not regular users of the protocol. Lowering to Medium. Great stuff though.
The finding in essence is claiming that you can setup an empty Basket
and sell it to external contracts, and those contracts would lose funds.
If that were the case the vulnerability would be in the "sniping / buying" contracts and not in the Basket nor the Vault.
The only thing the warden has shown is that they can create a Basket with a malicious token and through that they can call the Factory to create a Vault which after the tx will be empty.
This is logically equivalent to selling an empty vault, or selling a vault of BryptoPunks
(typo on purpose, it's a scam token)
The finding in essence is claiming that you can setup an empty
Basket
and sell it to external contracts, and those contracts would lose funds.If that were the case the vulnerability would be in the "sniping / buying" contracts and not in the Basket nor the Vault.
The only thing the warden has shown is that they can create a Basket with a malicious token and through that they can call the Factory to create a Vault which after the tx will be empty.
This is logically equivalent to selling an empty vault, or selling a vault of
BryptoPunks
(typo on purpose, it's a scam token)
I agree in terms of normal usage. The key here is a 3rd party contract uses on-chain logic in order to authorize a purchase. If that were the case, while in the middle of the attack as described all checks that contract may perform would confirm assets were included and terms look good -- it would not be able to determine that the basket was in the middle of a batch withdraw request. Let me know if I'm overlooking something
The basket itself does not need to hold a malicious token -- the withdraw request takes an array of addresses, so the malicious contract only need to appear there.
If that were the case, while in the middle of the attack as described all checks that contract may perform would confirm assets were included and terms look good -- it would not be able to determine that the basket was in the middle of a batch withdraw request. Let me know if I'm overlooking something
The 3rd party contract would need to check that the Basket is properly set via ownerOf(Basket) == Vault
(where Vault is an address contained in the list of nibbledVaults
from factory)
That would allow to determine if the contract is properly setup.
The basket itself does not need to hold a malicious token,
that is correct as you can setup any contract to accept the safeTransferFrom
call.
My statement is that an automated 3rd party contract can get rekt, but that's not the contract under audit
Agree that it's the 3rd party contract that suffers a loss.
An ownerOf(Basket) == Vault && ownerOf(NFT) == Basket
check is insufficient here because if it's in the middle of this scenario then the owner checks will appear legit but by the end of the tx they won't be. That's the part that I'm still hung up on. Part of the Medium definition is "the function of the protocol or its availability could be impacted" -- although not an explicit goal, is it not implicit that protocols can be built upon with other contracts. The concern here seems to limit that ability, one could not build a contract that decided to participate based on on-chain state alone w/ or w/o an allow list of NFTs -- it would require a trusted actor to allow list specific vaults or to perform the action itself.
This is certainly grey though. Very hypothetical, e.g. it's not clear that a 3rd party contract would ever be interested in a capability like this. This is an interesting discussion! I'll sleep on it, but please chime in if you have more to add - I appreciate the feedback.
Lines of code
https://github.com/code-423n4/2022-06-nibbl/blob/8c3dbd6adf350f35c58b31723d42117765644110/contracts/Basket.sol#L41-L47 https://github.com/code-423n4/2022-06-nibbl/blob/8c3dbd6adf350f35c58b31723d42117765644110/contracts/Basket.sol#L68-L75 https://github.com/code-423n4/2022-06-nibbl/blob/8c3dbd6adf350f35c58b31723d42117765644110/contracts/Basket.sol#L91-L97
Vulnerability details
Impact
Basket
is used for keep multiple tokens in contract and mint oneNFT
token to represent their ownership.Basket
only allows for owner ofNFT(id=0)
to withdraw tokens fromBasket
address. users can deposit multiple tokens in oneBasket
and then create aNibbVault
based on thatBasket NFT
. but due to reentrancy vulnerability inBasket
it's possible to call the multiple-token-withdraw functions (withdrawMultipleERC721()
,withdrawMultipleERC1155()
,withdrawMultipleERC721()
andwithdrawMultipleERC20()
) and in the middle their external calls, spendBasket NFT
(transfer ownership ofid=0
to other contract, for examplecreateVault()
) and receive some fund from other, then in the rest of the multiple-token-withdraw function withdraw all the basket tokens.Basket
shouldn't allow transferring ownership ofid=0
in the middle of multiple token withdraws.Proof of Concept
This is
withdrawMultipleERC721()
code:As you can see, contract only checks the ownership of
id=0
in the beginning of the function to see that user allowed to perform this action or not. then it iterates through user specified addresses and callsafeTransferFrom()
function in those address by user specified values. the bug is that in the middle of the external calls attacker can spendBasket NFT id=0
(give ownership of that basket to other contracts and receive fund from them, for example attacker can callcreateVault
inNibblVaultFactory
and create a vault and call other contracts to invest in that vault) then in the rest of the iterations inwithdrawMultipleERC721()
attacker can withdrawBasket
tokens. so even so the ownership of theBasket
has been transferred and attacker received funds for it, attacker withdrawBasket
tokens too. This is the steps attacker would perform:Basket
with well knownNFT
token list. let's assume theBasket
name isBasket_M
NibblVaultFactory
forBasket_M id=0
token.Basket_M.withdrawMultipleERC721(address[] memory _tokens, uint256[] memory _tokenId, address _to)
with list of all the tokens in basket to withdraw all of them, but the first address in the_tokens
list is the address that attacker controls.Basket_M
would check that attacker is owner of the basket (owner of theid = 0
) and in first iteration of thefor
it would call attacker controlled address which is a contract that attacker wrote its code.NibblVaultFactory.createVault()
withBasket_M
address andid=0
to create a vault which then transfer the ownership ofBasket_M id=0
to the vault address. let's assume it'sVault_M
.Vault_M
by callingbuy()
function.Invest_Contract
) that would want to buy fraction of the well knownNFT
s in the basket andInvest_Contract
invest some fund in vault having thoseNFT
in vault's address or vault's basket just by callingInvest_Contract
. attacker contract would callInvest_Contract
to invest inVault_M
andInvest_Contract
would check that well knownNFT
is inBasket_M id=0
which belongs toVault_M
to it would invest money on it by callinginitiateBuyout()
Vault_M
.Basket_M.withdrawMultipleERC721()
for iterations performs and all theNFT
tokens of theBasket_M
would be send to attacker andBasket_M
would have nothing.steps 5 to 8 can be other things, the point is in those steps attacker would spent
Basket_M
and receive some fund from other contract while those other contracts checks that they are owner of theBasket_M
which has well knownNFT
tokens, but in fact attacker withdraw those well knownNFT
tokens fromBasket_M
after spending it in the rest of thewithdrawMultipleERC721()
iterations. (those above step 5-8 is just a sample case)So
Basket
shouldn't allow ownership transfer in the middle of theBasket_M.withdrawMultipleERC721()
and similar multiple-token-withdraw functions or it should check the ownership in every iteration.Tools Used
VIM
Recommended Mitigation Steps
check ownership of
id=0
in every iteration or don't allow ownership transfer in the multiple-token-transfer functions