Beginner FriendlyFoundryGameFi
100 EXP
View results
Submission Details
Severity: medium
Invalid

Missing `fallback` or `receive` function in seller smart contract wallets, causes revert in `buyMartenitsa` function and prevents the buyer from purchasing the martenitsaToken.

Description

The MartenitsaMarketplace::buyMartenitsa function transfers the sales price to the seller using the seller.call{value: salePrice}("") low-level call. If the seller's smart contract wallet does not have a fallback or receive function to receive the ETH sent by the marketplace contract, the call will revert, preventing the buyer from purchasing the martenitsaToken.

function buyMartenitsa(uint256 tokenId) external payable {
.
.
.
.
// Transfer funds to seller
@> (bool sent,) = seller.call{value: salePrice}("");
require(sent, "Failed to send Ether");
// Transfer the token to the buyer
martenitsaToken.safeTransferFrom(seller, buyer, tokenId);
}

Impact

If the seller's smart contract wallet does not have a fallback or receive function to receive the ETH sent by the marketplace contract, the call to seller.call{value: salePrice}("") will revert, preventing the buyer from purchasing the martenitsaToken. This can lead to a poor user experience and may result in failed transactions.

Proof of Concept:

  1. A seller contract is created without a fallback or receive function.

  2. The seller contract lists a martenitsaToken for sale on the marketplace.

  3. A buyer attempts to purchase the martenitsaToken from the marketplace.

  4. The seller.call{value: salePrice}("") call reverts due to the lack of a fallback or receive function in the seller contract.

  5. The transaction fails, and the buyer is unable to purchase the martenitsaToken.

Proof Of Code
function test_SellerWithoutFallbackReverts() public {
// initialize the seller contract
SellerWithoutFallback sellerWithoutFallback =
new SellerWithoutFallback(address(marketplace), address(martenitsaToken));
// Set seller as producer
address[] memory producers = new address[](1);
producers[0] = address(sellerWithoutFallback);
martenitsaToken.setProducers(producers);
console.log("The seller is producer: ", martenitsaToken.isProducer(address(sellerWithoutFallback)));
// create martenitsa and list it for sale
sellerWithoutFallback.createAndListMartenitsaToken(0, "bracelet");
address user = makeAddr("user");
vm.deal(user, 1 wei);
// buy martenitsa
vm.startPrank(user);
console.log("The owner of the token before buying martenitsa: ", martenitsaToken.ownerOf(0));
console.log("The balance of the buyer before buying martenitsa: ", user.balance);
// As the seller doesnt have fallback function, the buyer doesnt get the token
vm.expectRevert();
marketplace.buyMartenitsa{value: 1 wei}(0);
console.log("The balance of the buyer after buying martenitsa: ", user.balance);
console.log("The owner of the token after buying martenitsa: ", martenitsaToken.ownerOf(0));
vm.stopPrank();
}

Here is the contract as well

contract SellerWithoutFallback {
MartenitsaMarketplace public marketplace;
MartenitsaToken public martenitsaToken;
constructor(address _marketplace, address _martenitsaToken) {
marketplace = MartenitsaMarketplace(_marketplace);
martenitsaToken = MartenitsaToken(_martenitsaToken);
}
function createAndListMartenitsaToken(uint256 tokenId, string memory design) public {
martenitsaToken.createMartenitsa(design);
marketplace.listMartenitsaForSale(tokenId, 1 wei);
martenitsaToken.approve(address(marketplace), tokenId);
}
function onERC721Received(address, address, uint256, bytes memory) public virtual returns (bytes4) {
return this.onERC721Received.selector;
}
}

Here is the foundry output

Logs:
The seller is producer: true
The owner of the token before buying martenitsa: 0xa0Cb889707d426A7A386870A03bc70d1b0697598
The balance of the buyer before buying martenitsa: 1
The balance of the buyer after buying martenitsa: 1
The owner of the token after buying martenitsa: 0xa0Cb889707d426A7A386870A03bc70d1b0697598

Recommended Mitigation

  1. Ensure that the seller's smart contract wallet includes a fallback or receive function to receive ETH sent by the marketplace contract. This function should handle the ETH transfer and any additional logic required by the seller contract.

  2. Provide clear documentation and guidance to users on the requirements for listing tokens for sale on the marketplace, including the need to implement a fallback or receive function in the seller contract.

  3. Implement checks in the MartenitsaMarketplace contract to detect whether the seller contract has a fallback or receive function and provide appropriate feedback to the user if the function is missing.

contract SellerWithoutFallback {
MartenitsaMarketplace public marketplace;
MartenitsaToken public martenitsaToken;
constructor(address _marketplace, address _martenitsaToken) {
marketplace = MartenitsaMarketplace(_marketplace);
martenitsaToken = MartenitsaToken(_martenitsaToken);
}
function createAndListMartenitsaToken(uint256 tokenId, string memory design) public {
martenitsaToken.createMartenitsa(design);
marketplace.listMartenitsaForSale(tokenId, 1 wei);
martenitsaToken.approve(address(marketplace), tokenId);
}
+ receive() external payable {}
+ fallback() external payable {}
function onERC721Received(address, address, uint256, bytes memory) public virtual returns (bytes4) {
return this.onERC721Received.selector;
}
}
Updates

Lead Judging Commences

bube Lead Judge over 1 year ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.