Description
If a user sends a msg.value
(the amount of ETH sent with the transaction) that is greater than the sales price of the MartenitsaToken
listed by a seller, the excess ETH gets stuck in the marketplace contract. This is because the contract does not have a mechanism to refund the excess ETH to the user.
function buyMartenitsa(uint256 tokenId) external payable {
Listing memory listing = tokenIdToListing[tokenId];
require(listing.forSale, "Token is not listed for sale");
@> require(msg.value >= listing.price, "Insufficient funds");
}
Impact
The excess ETH becomes locked in the MartenitsaMarketplace
contract, and the user is unable to retrieve it. This can lead to a loss of funds for the user and may result in a poor user experience. Additionally, the accumulation of excess ETH in the contract could potentially lead to a denial-of-service attack by filling up the contract's balance.
Tools Used
Manual Review
Proof of Concept
seller creates a martenitsaToken and lists it for sale.
buyer buys the martenitsaToken accidentally with more than the sales price of the martenitsaToken.
buyer gets the martenitsaToken and the seller gets the salesprice of the martenitsa.
excess amount eth of the buyer is stuck in the contract and not refunded to the buyer.
Proof Of Code
function test_BuyerSendsGreaterValueThanSalesPrice() public {
vm.startPrank(chasy);
martenitsaToken.createMartenitsa("bracelet");
marketplace.listMartenitsaForSale(0, 1 ether);
vm.stopPrank();
vm.prank(chasy);
martenitsaToken.approve(address(marketplace), 0);
address user = makeAddr("user");
vm.deal(user, 2 ether);
console.log("User balance before buying martenitsa: ", user.balance);
console.log("Marketplace balance before buying martenitsa: ", address(marketplace).balance);
console.log("Seller balance before buying martenitsa: ", chasy.balance);
vm.startPrank(user);
marketplace.buyMartenitsa{value: 2 ether}(0);
vm.stopPrank();
console.log("User balance after buying martenitsa: ", user.balance);
console.log("Marketplace balance after buying martenitsa: ", address(marketplace).balance);
console.log("Seller balance after buying martenitsa: ", chasy.balance);
console.log("Owner of the token after buying martenitsa: ", martenitsaToken.ownerOf(0));
}
Here is the foundry output
Logs:
User balance before buying martenitsa: 2000000000000000000
Marketplace balance before buying martenitsa: 0
Seller balance before buying martenitsa: 0
User balance after buying martenitsa: 0
Marketplace balance after buying martenitsa: 1000000000000000000
Seller balance after buying martenitsa: 1000000000000000000
Recommendations
To mitigate this issue, the marketplace contract should include a refund mechanism to send the excess ETH back to the user. One possible solution is to modify the MartenitsaMarketplace::buyMartenitsa
function to refund the excess ETH:
function buyMartenitsa(uint256 tokenId) external payable {
.
.
.
.
// Refund excess ETH to buyer
if (msg.value > salePrice) {
(bool refunded,) = msg.sender.call{value: msg.value - salePrice}("");
require(refunded, "Failed to refund excess Ether");
}
+ // 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);
}
By adding the refund logic, the marketplace contract ensures that any excess ETH sent by the user is returned to them and not getting stuck in the contract.