diff --git a/Makefile b/Makefile index 69ff72f..414995e 100644 --- a/Makefile +++ b/Makefile @@ -13,4 +13,4 @@ build: ## Build the smart contracts with foundry. .PHONY: test test: ## Run foundry unit tests. - @bash -l -c 'forge test' \ No newline at end of file + @bash -l -c 'forge test --no-match-path src/test/forks/**/*.sol' \ No newline at end of file diff --git a/src/auctionhouse/SuperRareAuctionHouse.sol b/src/auctionhouse/SuperRareAuctionHouse.sol index 66ba2ad..3d25101 100644 --- a/src/auctionhouse/SuperRareAuctionHouse.sol +++ b/src/auctionhouse/SuperRareAuctionHouse.sol @@ -49,27 +49,33 @@ contract SuperRareAuctionHouse is _checkSplits(_splitAddresses, _splitRatios); _checkValidAuctionType(_auctionType); - require(_lengthOfAuction <= maxAuctionLength, "configureAuction::Auction too long."); + { + require(_lengthOfAuction <= maxAuctionLength, "configureAuction::Auction too long."); - Auction memory auction = tokenAuctions[_originContract][_tokenId]; + Auction memory auction = tokenAuctions[_originContract][_tokenId]; - require( - auction.auctionType == NO_AUCTION || auction.auctionCreator != msg.sender, - "configureAuction::Cannot have a current auction." - ); + Bid memory staleBid = auctionBids[_originContract][_tokenId]; - require(_lengthOfAuction > 0, "configureAuction::Length must be > 0"); + require(staleBid.bidder == address(0), "configureAuction::bid shouldnt exist"); - if (_auctionType == COLDIE_AUCTION) { - require(_startingAmount > 0, "configureAuction::Coldie starting price must be > 0"); - } else if (_auctionType == SCHEDULED_AUCTION) { - require(_startTime > block.timestamp, "configureAuction::Scheduled auction cannot start in past."); - } + require( + auction.auctionType == NO_AUCTION || (auction.auctionCreator != msg.sender), + "configureAuction::Cannot have a current auction" + ); - require( - _startingAmount <= marketplaceSettings.getMarketplaceMaxValue(), - "configureAuction::Cannot set starting price higher than max value." - ); + require(_lengthOfAuction > 0, "configureAuction::Length must be > 0"); + + if (_auctionType == COLDIE_AUCTION) { + require(_startingAmount > 0, "configureAuction::Coldie starting price must be > 0"); + } else if (_auctionType == SCHEDULED_AUCTION) { + require(_startTime > block.timestamp, "configureAuction::Scheduled auction cannot start in past."); + } + + require( + _startingAmount <= marketplaceSettings.getMarketplaceMaxValue(), + "configureAuction::Cannot set starting price higher than max value." + ); + } tokenAuctions[_originContract][_tokenId] = Auction( payable(msg.sender), @@ -130,6 +136,11 @@ contract SuperRareAuctionHouse is "convertOfferToAuction::Cannot have a current auction." ); + require( + auction.startingTime == 0 || block.timestamp < auction.startingTime, + "convertOfferToAuction::Auction must not have started." + ); + require(_lengthOfAuction <= maxAuctionLength, "convertOfferToAuction::Auction too long."); Offer memory currOffer = tokenCurrentOffers[_originContract][_tokenId][_currencyAddress]; @@ -204,6 +215,8 @@ contract SuperRareAuctionHouse is erc721.transferFrom(address(this), msg.sender, _tokenId); } + require(erc721.ownerOf(_tokenId) == msg.sender, "sending failed"); + emit CancelAuction(_originContract, _tokenId, auction.auctionCreator); } @@ -330,7 +343,7 @@ contract SuperRareAuctionHouse is _payout( _originContract, _tokenId, - auction.currencyAddress, + currBid.currencyAddress, currBid.amount, auction.auctionCreator, auction.splitRecipients, @@ -340,6 +353,8 @@ contract SuperRareAuctionHouse is marketplaceSettings.markERC721Token(_originContract, _tokenId, true); } + require(erc721.ownerOf(_tokenId) == currBid.bidder, "sending failed"); + emit AuctionSettled( _originContract, currBid.bidder, @@ -356,21 +371,14 @@ contract SuperRareAuctionHouse is /** @return Auction Struct: creatorAddress, creationTime, startingTime, lengthOfAuction, currencyAddress, minimumBid, auctionType, splitRecipients array, and splitRatios array. */ - function getAuctionDetails(address _originContract, uint256 _tokenId) + function getAuctionDetails( + address _originContract, + uint256 _tokenId + ) external view override - returns ( - address, - uint256, - uint256, - uint256, - address, - uint256, - bytes32, - address payable[] memory, - uint8[] memory - ) + returns (address, uint256, uint256, uint256, address, uint256, bytes32, address payable[] memory, uint8[] memory) { Auction memory auction = tokenAuctions[_originContract][_tokenId]; @@ -392,4 +400,4 @@ contract SuperRareAuctionHouse is revert("Invalid Auction Type"); } } -} +} \ No newline at end of file diff --git a/src/test/bazaar/SuperRareBazaar.t.sol b/src/test/bazaar/SuperRareBazaar.t.sol new file mode 100644 index 0000000..94e7e4d --- /dev/null +++ b/src/test/bazaar/SuperRareBazaar.t.sol @@ -0,0 +1,242 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.0; + +import {Test, console2} from "forge-std/Test.sol"; + +import {ISuperRareBazaar, SuperRareBazaar} from "../../bazaar/SuperRareBazaar.sol"; +import {ISuperRareMarketplace, SuperRareMarketplace} from "../../marketplace/SuperRareMarketplace.sol"; +import {IMarketplaceSettings} from "rareprotocol/aux/marketplace/IMarketplaceSettings.sol"; +import {IStakingSettings} from "rareprotocol/aux/marketplace/IStakingSettings.sol"; +import {IRoyaltyRegistry} from "rareprotocol/aux/registry/interfaces/IRoyaltyRegistry.sol"; +import {IPayments} from "rareprotocol/aux/payments/IPayments.sol"; +import {Payments} from "rareprotocol/aux/payments/Payments.sol"; +import {ISpaceOperatorRegistry} from "rareprotocol/aux/registry/interfaces/ISpaceOperatorRegistry.sol"; +import {IApprovedTokenRegistry} from "rareprotocol/aux/registry/interfaces/IApprovedTokenRegistry.sol"; +import {IRoyaltyEngineV1} from "royalty-registry/IRoyaltyEngineV1.sol"; +import {Payments} from "rareprotocol/aux/payments/Payments.sol"; +import {ISuperRareAuctionHouse, SuperRareAuctionHouse} from "../../auctionhouse/SuperRareAuctionHouse.sol"; +import {IRareStakingRegistry} from "../../staking/registry/IRareStakingRegistry.sol"; +import {IERC721} from "openzeppelin-contracts/token/ERC721/IERC721.sol"; +import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol"; +import {SuperFakeNFT} from "../../test/utils/SuperFakeNFT.sol"; +import {TestRare} from "../../test/utils/TestRare.sol"; + + + +contract SuperRareBazaarTest is Test { + TestRare private superRareToken; + SuperRareMarketplace private superRareMarketplace; + SuperRareAuctionHouse private superRareAuctionHouse; + SuperRareBazaar private superRareBazaar; + + + address marketplaceSettings = address(0xabadaba1); + address royaltyRegistry = address(0xabadaba2); + address royaltyEngine = address(0xabadaba3); + address spaceOperatorRegistry = address(0xabadaba6); + address approvedTokenRegistry = address(0xabadaba7); + address stakingRegistry = address(0xabadaba9); + address networkBeneficiary = address(0xabadabaa); + address rewardPool = address(0xcccc); + + address private immutable exploiter = vm.addr(0x123); + address private immutable exploiter1 = vm.addr(0x231); + address private immutable bidder = vm.addr(0x321); + + uint256 private constant TARGET_AMOUNT = 249.6 ether; + + uint256 private constant _lengthOfAuction = 1; + + bytes32 private constant SCHEDULED_AUCTION = "SCHEDULED_AUCTION"; + + SuperFakeNFT private sfn; + + function setUp() public { + // Create market, auction, bazaar, and token contracts + superRareToken = new TestRare(); + superRareMarketplace = new SuperRareMarketplace(); + superRareAuctionHouse = new SuperRareAuctionHouse(); + superRareBazaar = new SuperRareBazaar(); + + // Deploy Payments + Payments payments = new Payments(); + + // Initialize the bazaar + superRareBazaar.initialize(marketplaceSettings, royaltyRegistry, royaltyEngine, address(superRareMarketplace), address(superRareAuctionHouse), spaceOperatorRegistry, approvedTokenRegistry, address(payments), stakingRegistry, networkBeneficiary); + + SuperFakeNFT _sfn = new SuperFakeNFT(address(superRareBazaar)); + sfn = _sfn; + + sfn.mint(exploiter, 1); + superRareToken.transfer(bidder, 300 ether); + vm.deal(address(superRareBazaar), 300 ether); + + vm.prank(bidder); + superRareToken.approve(address(superRareBazaar), type(uint256).max); + + vm.prank(exploiter); + sfn.setApprovalForAll(address(superRareBazaar), true); + + vm.prank(exploiter1); + sfn.setApprovalForAll(address(superRareBazaar), true); + + // etch code into these so we can stub out methods. Need some + vm.etch(marketplaceSettings, address(superRareToken).code); + vm.etch(stakingRegistry, address(superRareToken).code); + vm.etch(royaltyRegistry, address(superRareToken).code); + vm.etch(royaltyEngine, address(superRareToken).code); + vm.etch(spaceOperatorRegistry, address(superRareToken).code); + vm.etch(approvedTokenRegistry, address(superRareToken).code); + } + + function test_auctions_with_eth_sucess() public { + + } + + function test_auctions_with_erc20_success() public { + + } + + function test_convert_offer_currency_exploit() external { + + /*/////////////////////////////////////////////////// + Mock Calls + ///////////////////////////////////////////////////*/ + vm.mockCall( + stakingRegistry, + abi.encodeWithSelector(IRareStakingRegistry.getRewardAccumulatorAddressForUser.selector, exploiter1), + abi.encode(address(0)) + ); + vm.mockCall( + marketplaceSettings, + abi.encodeWithSelector(IStakingSettings.calculateMarketplacePayoutFee.selector, TARGET_AMOUNT), + abi.encode((TARGET_AMOUNT * 3) / 100) + ); + vm.mockCall( + marketplaceSettings, + abi.encodeWithSelector(IStakingSettings.calculateStakingFee.selector, TARGET_AMOUNT), + abi.encode(0) + ); + vm.mockCall( + marketplaceSettings, + abi.encodeWithSelector(IMarketplaceSettings.getMarketplaceFeePercentage.selector), + abi.encode(3) + ); + vm.mockCall( + marketplaceSettings, + abi.encodeWithSelector(IMarketplaceSettings.getMarketplaceMaxValue.selector), + abi.encode(type(uint256).max) + ); + vm.mockCall( + marketplaceSettings, + abi.encodeWithSelector(IMarketplaceSettings.calculateMarketplaceFee.selector, TARGET_AMOUNT), + abi.encode((TARGET_AMOUNT * 3) / 100) + ); + vm.mockCall( + marketplaceSettings, + abi.encodeWithSelector(IMarketplaceSettings.hasERC721TokenSold.selector, address(sfn), 1), + abi.encode(false) + ); + vm.mockCall( + spaceOperatorRegistry, + abi.encodeWithSelector(ISpaceOperatorRegistry.isApprovedSpaceOperator.selector, exploiter1), + abi.encode(false) + ); + vm.mockCall( + approvedTokenRegistry, + abi.encodeWithSelector(IApprovedTokenRegistry.isApprovedToken.selector, address(superRareToken)), + abi.encode(true) + ); + vm.mockCall( + marketplaceSettings, + abi.encodeWithSelector(IMarketplaceSettings.getERC721ContractPrimarySaleFeePercentage.selector, address(sfn)), + abi.encode(15) + ); + vm.mockCall( + marketplaceSettings, + abi.encodeWithSelector(IMarketplaceSettings.markERC721Token.selector, address(sfn)), + abi.encode() + ); + + /*/////////////////////////////////////////////////// + Test + ///////////////////////////////////////////////////*/ + configureAuction(); + + skip(12); //~about 1 block + vm.expectRevert(); + superRareBazaar.settleAuction(address(sfn), 1); + } + + + /*////////////////////////////////////////////////////////////////////////// + Helper Functions + //////////////////////////////////////////////////////////////////////////*/ + + // Receive function for test contract to be sent value + receive() external payable { + console2.log("Amount Recieved by Attacker:", msg.value); + } + + // Configure the auction + function configureAuction() internal { + // Setup the Offer and convert it to an auction + createOfferAndConvertToAuction(); + + address payable[] memory _splitAddresses = new address payable[](1); + _splitAddresses[0] = payable(address(this)); + + uint8[] memory _splitRatios = new uint8[](1); + _splitRatios[0] = 100; + + //@exploit: Assumes all NFTs follows the ERC-721 spec + vm.prank(exploiter); + IERC721(sfn).transferFrom(exploiter, exploiter1, 1); + + //@exploit: Overwrites previously set auction with a new Currency (ETH). Keeps the same bid + vm.prank(exploiter1); + vm.expectRevert(); + superRareBazaar.configureAuction( + SCHEDULED_AUCTION, + address(sfn), + 1, + TARGET_AMOUNT, + address(0), + _lengthOfAuction, + block.timestamp + 1, + _splitAddresses, + _splitRatios + ); + } + + function createOfferAndConvertToAuction() internal { + createOffer(); + + address payable[] memory _splitAddresses = new address payable[](1); + _splitAddresses[0] = payable(address(this)); + + uint8[] memory _splitRatios = new uint8[](1); + _splitRatios[0] = 100; + + vm.prank(exploiter); + superRareBazaar.convertOfferToAuction( + address(sfn), + 1, + address(superRareToken), + TARGET_AMOUNT, + _lengthOfAuction, + _splitAddresses, + _splitRatios + ); + } + + function createOffer() internal { + console2.log("Before Attack: SuperRareBazaar ETH Balance:", address(superRareBazaar).balance); + + //@exploit: Create an Offer using a custom NFT and the superRareToken as Currency + vm.prank(bidder); + superRareBazaar.offer(address(sfn), 1, address(superRareToken), TARGET_AMOUNT, true); + } + + +} \ No newline at end of file diff --git a/src/test/forks/mainnet/18029585/bazaar/SuperRareBazaarAuctionUpgrade.t.sol b/src/test/forks/mainnet/18029585/bazaar/SuperRareBazaarAuctionUpgrade.t.sol new file mode 100644 index 0000000..d915e50 --- /dev/null +++ b/src/test/forks/mainnet/18029585/bazaar/SuperRareBazaarAuctionUpgrade.t.sol @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.0; + +import {Test, console2} from "forge-std/Test.sol"; + +import {ISuperRareBazaar, SuperRareBazaar} from "../../../../../bazaar/SuperRareBazaar.sol"; +import {ISuperRareMarketplace, SuperRareMarketplace} from "../../../../../marketplace/SuperRareMarketplace.sol"; +import {ISuperRareAuctionHouse, SuperRareAuctionHouse} from "../../../../../auctionhouse/SuperRareAuctionHouse.sol"; +import {IRareStakingRegistry} from "../../../../../staking/registry/IRareStakingRegistry.sol"; +import {IERC721} from "openzeppelin-contracts/token/ERC721/IERC721.sol"; +import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol"; +import {SuperFakeNFT} from "../../../../../test/utils/SuperFakeNFT.sol"; +import {TestRare} from "../../../../../test/utils/TestRare.sol"; + +contract SuperRareBazaarAuctionUpgrade is Test { + // Constant auction length for testing + uint256 private constant LENGTH_OF_AUCTION = 12; + + // Auction Type for testing + bytes32 private constant SCHEDULED_AUCTION = "SCHEDULED_AUCTION"; + + // SuperRareToken Contract + address private rare; + + // Bazaar Contract + SuperRareBazaar private bazaar; + + // Updated Auction Contract + SuperRareAuctionHouse private auctionHouse; + + // Contract Admin + address private admin; + + // Bidder + address private bidder; + + // Auction Creator + address private auctionCreator; + + // NFT Contract + IERC721 private nftContract; + + // NFT Token ID + uint256 private tokenId; + + /*/////////////////////////////////////////////////// + Setup + ///////////////////////////////////////////////////*/ + function setUp() public { + // Check that it is mainnet and the block is correct + require( + block.number == 18029585, + "This test is intended to be run against a mainnet fork at block: 18029585. Please run using: forge test --fork-url --fork-block-number 18029585" + ); + + // EOAs + admin = address(0x860a80d33E85e97888F1f0C75c6e5BBD60b48DA9); + bidder = address(0x09F8b58438C026564CbC23d54fBa39C7237D827A); + auctionCreator = address(0xec8c1050B45789f9ee4D09dCC7D64aAF9e233338); + vm.deal(admin, 10 ether); + vm.deal(auctionCreator, 10 ether); + vm.deal(bidder, 10 ether); + + // Contracts + rare = address(0xba5BDe662c17e2aDFF1075610382B9B691296350); + bazaar = SuperRareBazaar(address(0x6D7c44773C52D396F43c2D511B81aa168E9a7a42)); + auctionHouse = new SuperRareAuctionHouse(); + nftContract = IERC721(address(0xE418c30CA2ECD3C046122ea0FaF95a6B9DA97191)); + tokenId = 137; + + // Set Approval for Bazaar + vm.prank(auctionCreator); + nftContract.setApprovalForAll(address(bazaar), true); + } + + /*/////////////////////////////////////////////////// + Tests + ///////////////////////////////////////////////////*/ + + // Test that a running auction can still settle + function test_running_auction_settles_after_upgrade() public { + address payable[] memory splitAddresses = new address payable[](1); + splitAddresses[0] = payable(auctionCreator); + + uint8[] memory splitRatios = new uint8[](1); + splitRatios[0] = 100; + // Create an auction + vm.prank(auctionCreator); + bazaar.configureAuction( + SCHEDULED_AUCTION, + address(nftContract), + tokenId, + 1 ether, + address(0), + LENGTH_OF_AUCTION, + block.timestamp + 1, + splitAddresses, + splitRatios + ); + + // Auction begins + vm.warp(block.timestamp + 2); + + // Bid + vm.prank(bidder); + bazaar.bid{value: 1.03 ether}(address(nftContract), tokenId, address(0), 1 ether); + + // Upgrade happens + runBazaarUpgrade(); + + // Auction ends + vm.warp(block.timestamp + 100*LENGTH_OF_AUCTION + 1); + + // Auction settled + bazaar.settleAuction(address(nftContract), tokenId); + } + function test_create_auction_after_upgrade() public { + // Upgrade happens + runBazaarUpgrade(); + + address payable[] memory splitAddresses = new address payable[](1); + splitAddresses[0] = payable(auctionCreator); + + uint8[] memory splitRatios = new uint8[](1); + splitRatios[0] = 100; + // Create an auction + vm.prank(auctionCreator); + bazaar.configureAuction( + SCHEDULED_AUCTION, + address(nftContract), + tokenId, + 1 ether, + address(0), + LENGTH_OF_AUCTION, + block.timestamp + 1, + splitAddresses, + splitRatios + ); + + // Auction begins + vm.warp(block.timestamp + 2); + + // Bid + vm.prank(bidder); + bazaar.bid{value: 1.03 ether}(address(nftContract), tokenId, address(0), 1 ether); + + + // Auction ends + vm.warp(block.timestamp + 100*LENGTH_OF_AUCTION + 1); + + // Auction settled + bazaar.settleAuction(address(nftContract), tokenId); + } + + /*/////////////////////////////////////////////////// + Helper Functions + ///////////////////////////////////////////////////*/ + + // Function to run the upgrade + function runBazaarUpgrade() public { + vm.startPrank(admin); + bazaar.setSuperRareAuctionHouse(address(auctionHouse)); + vm.stopPrank(); + } +} diff --git a/src/test/utils/SuperFakeNFT.sol b/src/test/utils/SuperFakeNFT.sol new file mode 100644 index 0000000..43070ba --- /dev/null +++ b/src/test/utils/SuperFakeNFT.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.0; + +import {ERC721} from "openzeppelin-contracts/token/ERC721/ERC721.sol"; + +contract SuperFakeNFT is ERC721("Super Fake", "SUPRFKE") { + address private bazaar; + + constructor(address _bazaar) { + bazaar = _bazaar; + } + + function mint(address to, uint256 tokenId) external { + _mint(to, tokenId); + } + + function transferFrom(address from, address to, uint256 tokenId) public override { + if (msg.sender == bazaar) { + return; + } + + super.transferFrom(from, to, tokenId); + } +} \ No newline at end of file diff --git a/src/test/utils/TestRare.sol b/src/test/utils/TestRare.sol new file mode 100644 index 0000000..8ac6923 --- /dev/null +++ b/src/test/utils/TestRare.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.0; + +import {ERC20} from "openzeppelin-contracts/token/ERC20/ERC20.sol"; + +contract TestRare is ERC20 { + constructor() ERC20("Rare", "RARE") { + _mint(msg.sender, 1_000_000_000 ether); + } + + function burn(uint256 amount) public { + _burn(msg.sender, amount); + } +} \ No newline at end of file