Skip to content

Instantly share code, notes, and snippets.

@romeroadrian
Created April 13, 2023 18:11
Show Gist options
  • Save romeroadrian/06238839330315780b90d9202042ea0f to your computer and use it in GitHub Desktop.
Save romeroadrian/06238839330315780b90d9202042ea0f to your computer and use it in GitHub Desktop.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "solmate/tokens/ERC721.sol";
import "solmate/tokens/ERC20.sol";
import {IERC3156FlashBorrower} from "openzeppelin/interfaces/IERC3156FlashLender.sol";
import "./Fixture.sol";
contract GUSD is ERC20 {
constructor() ERC20("Gemini dollar", "GUSD", 2) {}
function mint(address account, uint256 amount) external {
_mint(account, amount);
}
}
contract MockToken is ERC20 {
constructor() ERC20("Mock ERC20", "MOCK", 18) {}
function mint(address account, uint256 amount) external {
_mint(account, amount);
}
}
contract RoyaltyLookupNoERC165 {
function royaltyInfo(
uint256 tokenId,
uint256 salePrice
) external returns (address recipient, uint256 amount) {
recipient = address(0xbeef);
amount = 42;
}
}
contract FlashLoanBorrower is IERC3156FlashBorrower, ERC721TokenReceiver {
function onFlashLoan(
address initiator,
address token,
uint256 amount,
uint256 fee,
bytes calldata data
) external returns (bytes32) {
console.log("Fee for flash loan:", fee);
// Allow pool to pull NFT
ERC721(token).setApprovalForAll(msg.sender, true);
return keccak256("ERC3156FlashBorrower.onFlashLoan");
}
}
contract AuditTest is Fixture {
address alice;
address bob;
address charlie;
address royaltyRegistryOwner;
function setUp() public {
alice = makeAddr("Alice");
bob = makeAddr("Bob");
charlie = makeAddr("Charlie");
royaltyRegistryOwner = makeAddr("RoyaltyRegistryOwner");
royaltyRegistry.initialize(royaltyRegistryOwner);
}
function test_Factory_create_UninitializedImplementation() public {
// Factory with uninitialized pool implementation
Factory factory = new Factory();
// Setup
vm.deal(bob, 1e18);
// Bob will create a pool and loose funds
vm.startPrank(bob);
uint256[] memory tokenIds = new uint256[](0);
PrivatePool privatePool = factory.create{value: 1e18}(
address(0), // address _baseToken
address(milady), // address _nft
100e18, // uint128 _virtualBaseTokenReserves
10e18, // uint128 _virtualNftReserves
200, // uint56 _changeFee
100, // uint16 _feeRate
bytes32(0), // bytes32 _merkleRoot
false, // bool _useStolenNftOracle
false, // bool _payRoyalties
bytes32(0), // bytes32 _salt
tokenIds, // uint256[] memory tokenIds
1e18 // uint256 baseTokenAmount
);
// Funds are in the proxy but implementation is address(0)
assertEq(address(privatePool).balance, 1e18);
// Bob tries to withdraw funds but the call won't do anything as there is no implementation
privatePool.withdraw(address(milady), tokenIds, address(0), 1e18);
// Funds are still in the pool
assertEq(address(privatePool).balance, 1e18);
vm.stopPrank();
}
function test_PrivatePool_changeFeeQuote_LowDecimalToken() public {
// Create a pool with GUSD which has 2 decimals
ERC20 gusd = new GUSD();
PrivatePool privatePool = new PrivatePool(
address(factory),
address(royaltyRegistry),
address(stolenNftOracle)
);
privatePool.initialize(
address(gusd), // address _baseToken,
address(milady), // address _nft,
100e18, // uint128 _virtualBaseTokenReserves,
10e18, // uint128 _virtualNftReserves,
500, // uint56 _changeFee,
100, // uint16 _feeRate,
bytes32(0), // bytes32 _merkleRoot,
false, // bool _useStolenNftOracle,
false // bool _payRoyalties
);
// The following will fail due an overflow. Calls to `change` function will always revert.
vm.expectRevert();
privatePool.changeFeeQuote(1e18);
}
function test_PrivatePool_getRoyalty_RevertCheckInterface() public {
RoyaltyLookupNoERC165 royaltyLookup = new RoyaltyLookupNoERC165();
// Register royalty lookup for milady collection
vm.prank(royaltyRegistryOwner);
royaltyRegistry.setRoyaltyLookupAddress(
address(milady),
address(royaltyLookup)
);
// Setup pool
PrivatePool privatePool = new PrivatePool(
address(factory),
address(royaltyRegistry),
address(stolenNftOracle)
);
privatePool.initialize(
address(0), // address _baseToken,
address(milady), // address _nft,
100e18, // uint128 _virtualBaseTokenReserves,
10e18, // uint128 _virtualNftReserves,
500, // uint56 _changeFee,
100, // uint16 _feeRate,
bytes32(0), // bytes32 _merkleRoot,
false, // bool _useStolenNftOracle,
true // bool _payRoyalties
);
// Add NFT to pool
uint256 tokenId = 0;
milady.mint(address(privatePool), tokenId);
// Alice tries to buy the NFT
vm.startPrank(alice);
(uint256 netInputAmount, , ) = privatePool.buyQuote(1e18);
vm.deal(alice, netInputAmount);
uint256[] memory tokenIds = new uint256[](1);
tokenIds[0] = tokenId;
uint256[] memory tokenWeights = new uint256[](0);
PrivatePool.MerkleMultiProof memory proof;
// The following will fail because `_getRoyalty` will try to call function `supportsInterface` in RoyaltyLookupNoERC165
vm.expectRevert();
privatePool.buy{value: netInputAmount}(tokenIds, tokenWeights, proof);
vm.stopPrank();
}
function test_EthRouter_sell_DoesntValidatePoolUsesEth() public {
// Setup pool with ERC20
MockToken erc20 = new MockToken();
PrivatePool privatePool = new PrivatePool(
address(factory),
address(royaltyRegistry),
address(stolenNftOracle)
);
privatePool.initialize(
address(erc20), // address _baseToken,
address(milady), // address _nft,
100e18, // uint128 _virtualBaseTokenReserves,
10e18, // uint128 _virtualNftReserves,
0, // uint56 _changeFee,
0, // uint16 _feeRate,
bytes32(0), // bytes32 _merkleRoot,
false, // bool _useStolenNftOracle,
false // bool _payRoyalties
);
erc20.mint(address(privatePool), 100e18);
// Alice will mistakenly use the EthRouter to sell an NFT to the pool
vm.startPrank(alice);
uint256 tokenId = 0;
milady.mint(alice, tokenId);
EthRouter.Sell[] memory sells = new EthRouter.Sell[](1);
sells[0].pool = payable(privatePool);
sells[0].nft = address(milady);
sells[0].tokenIds = new uint256[](1);
sells[0].tokenIds[0] = tokenId;
sells[0].isPublicPool = false;
milady.setApprovalForAll(address(ethRouter), true);
// The following action will succeed even though the pool is not using ETH as the base token
ethRouter.sell(sells, 0, 0, false);
// NFT will be transferred to pool
assertEq(milady.ownerOf(tokenId), address(privatePool));
// And tokens will be stuck in the EthRouter
assertTrue(erc20.balanceOf(address(ethRouter)) > 0);
vm.stopPrank();
}
function test_EthRouter_change_IncorrectCallValue() public {
// Setup pools
PrivatePool privatePool1 = new PrivatePool(
address(factory),
address(royaltyRegistry),
address(stolenNftOracle)
);
privatePool1.initialize(
address(0), // address _baseToken,
address(milady), // address _nft,
100e18, // uint128 _virtualBaseTokenReserves,
10e18, // uint128 _virtualNftReserves,
25, // uint56 _changeFee,
0, // uint16 _feeRate,
bytes32(0), // bytes32 _merkleRoot,
false, // bool _useStolenNftOracle,
false // bool _payRoyalties
);
PrivatePool privatePool2 = new PrivatePool(
address(factory),
address(royaltyRegistry),
address(stolenNftOracle)
);
privatePool2.initialize(
address(0), // address _baseToken,
address(milady), // address _nft,
100e18, // uint128 _virtualBaseTokenReserves,
10e18, // uint128 _virtualNftReserves,
25, // uint56 _changeFee,
0, // uint16 _feeRate,
bytes32(0), // bytes32 _merkleRoot,
false, // bool _useStolenNftOracle,
false // bool _payRoyalties
);
// pool 1 has milady 1, pool 2 has milady 2
uint256 tokenId1 = 1;
uint256 tokenId2 = 2;
milady.mint(address(privatePool1), tokenId1);
milady.mint(address(privatePool2), tokenId2);
// Now alice tries to change both tokens from both pools. Alice has milady 3 and 4
vm.startPrank(alice);
vm.deal(alice, 1 ether);
uint256 tokenId3 = 3;
uint256 tokenId4 = 4;
milady.mint(address(alice), tokenId3);
milady.mint(address(alice), tokenId4);
milady.setApprovalForAll(address(ethRouter), true);
EthRouter.Change[] memory changes = new EthRouter.Change[](2);
changes[0].pool = payable(privatePool1);
changes[0].nft = address(milady);
changes[0].inputTokenIds = new uint256[](1);
changes[0].inputTokenIds[0] = tokenId3;
changes[0].outputTokenIds = new uint256[](1);
changes[0].outputTokenIds[0] = tokenId1;
changes[1].pool = payable(privatePool2);
changes[1].nft = address(milady);
changes[1].inputTokenIds = new uint256[](1);
changes[1].inputTokenIds[0] = tokenId4;
changes[1].outputTokenIds = new uint256[](1);
changes[1].outputTokenIds[0] = tokenId2;
// Each change will take 0.0025 ETH
uint256 value = 2* 0.0025 ether;
// The following action will fail even though everything should be correct. The main issue is that the `change` funtion will try to forward `msg.value` to each call of the PrivatePool.change. The first call will take the fee (0.0025) and refund the excess (0.0025), but the second call will try to forward again 0.0050 ETH and will fail since the balance is only 0.0025 ETH.
vm.expectRevert();
ethRouter.change{value: value}(changes, 0);
vm.stopPrank();
}
function test_EthRouter_deposit_AnyoneCanDeposit() public {
// Alice creates a pool
uint256[] memory tokenIds;
vm.prank(alice);
PrivatePool privatePool = factory.create(
address(0), // address _baseToken
address(milady), // address _nft
100e18, // uint128 _virtualBaseTokenReserves
10e18, // uint128 _virtualNftReserves
0, // uint56 _changeFee
0, // uint16 _feeRate
bytes32(0), // bytes32 _merkleRoot
false, // bool _useStolenNftOracle
false, // bool _payRoyalties
bytes32(0), // bytes32 _salt
tokenIds, // uint256[] memory tokenIds
0 // uint256 baseTokenAmount
);
// However, Bob is allowed to deposit in this pool and will loose all funds
vm.startPrank(bob);
vm.deal(bob, 1 ether);
uint256 tokenId = 0;
milady.mint(bob, tokenId);
milady.setApprovalForAll(address(ethRouter), true);
tokenIds = new uint256[](1);
tokenIds[0] = tokenId;
ethRouter.deposit(
payable(privatePool), // address payable privatePool,
address(milady), // address nft,
tokenIds, // uint256[] calldata tokenIds,
0, // uint256 minPrice,
type(uint256).max, // uint256 maxPrice,
0 // uint256 deadline
);
vm.stopPrank();
}
function test_PrivatePool_flashLoan_IncorrectFee() public {
// Setup pool
PrivatePool privatePool = new PrivatePool(
address(factory),
address(royaltyRegistry),
address(stolenNftOracle)
);
uint56 changeFee = 25;
privatePool.initialize(
address(0), // address _baseToken,
address(milady), // address _nft,
100e18, // uint128 _virtualBaseTokenReserves,
10e18, // uint128 _virtualNftReserves,
changeFee, // uint56 _changeFee,
0, // uint16 _feeRate,
bytes32(0), // bytes32 _merkleRoot,
false, // bool _useStolenNftOracle,
false // bool _payRoyalties
);
uint256 tokenId = 0;
milady.mint(address(privatePool), tokenId);
// Alice executes a flash loan
vm.startPrank(alice);
FlashLoanBorrower flashLoanBorrower = new FlashLoanBorrower();
// Alice just sends 25 wei!
vm.deal(alice, changeFee);
privatePool.flashLoan{value: changeFee}(flashLoanBorrower, address(milady), tokenId, "");
vm.stopPrank();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment