sherlock-audit / 2024-04-titles-judging

12 stars 9 forks source link

fugazzi - Edition minting is supposed to refund unused fees to caller but forwards all value to FeeManager #164

Closed sherlock-admin3 closed 6 months ago

sherlock-admin3 commented 6 months ago

fugazzi

medium

Edition minting is supposed to refund unused fees to caller but forwards all value to FeeManager

Summary

The different variants of mint in the Edition are supposed to refund the caller with the unused amount of ETH. However, since all ETH is forwarded to the FeeManager, no refunds are issued as the Edition contract is always empty.

Vulnerability Detail

All of the different variants of the mint function in the Edition contract expect to refund the caller about unused ETH via the _refundExcess() internal function. For example, this is the implementation one of the mint() functions:

function mint(
    address to_,
    uint256 tokenId_,
    uint256 amount_,
    address referrer_,
    bytes calldata data_
) external payable override {
    // wake-disable-next-line reentrancy
    FEE_MANAGER.collectMintFee{value: msg.value}(
        this, tokenId_, amount_, msg.sender, referrer_, works[tokenId_].strategy
    );

    _issue(to_, tokenId_, amount_, data_);
    _refundExcess();
}
function _refundExcess() internal {
    if (msg.value > 0 && address(this).balance > 0) {
        msg.sender.safeTransferETH(address(this).balance);
    }
}

The implementation of _refundExcess() correctly transfers ETH back to the caller. However, all ETH is transferred in the call to collectMintFee(), and the FeeManager doesn't issue any refund.

This means that all mint variants of mint (mint(), mintWithComment(), mintBatch()) will transfer all ETH to the FeeManager, breaking refunds as the Edition contract will have null balance, even if there was an actual excess in fees.

Proof of concept

function test_missing_refund() public {
    Edition edition = new Edition();
    FeeManager feeManager = new FeeManager(address(0xdeadbeef), address(0xc0ffee), address(new MockSplitFactory()));
    TitlesGraph graph = new TitlesGraph(address(this), address(this));

    edition.initialize(
        feeManager,
        graph,
        address(this),
        address(this),
        Metadata({label: "Test Edition", uri: "ipfs.io/test-edition", data: new bytes(0)})
    );

    edition.publish(
        address(1), // creator
        10, // maxSupply
        0, // opensAt
        0, // closesAt
        new Node[](0), // attributions
        Strategy({
            asset: address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE),
            mintFee: 0.01 ether,
            revshareBps: 2500, // 25%
            royaltyBps: 250 // 2.5%
        }),
        Metadata({label: "Best Work Ever", uri: "ipfs.io/best-work-ever", data: new bytes(0)})
    );

    // Normally done by the TitlesCore, but we're testing in isolation
    feeManager.createRoute(edition, 1, new Target[](0), address(0));

    address minter = makeAddr("minter");

    vm.deal(minter, 1 ether);

    vm.prank(minter);
    edition.mint{value: 1 ether}(minter, 1, 1, address(0), new bytes(0));

    // minter didn't get any refund
    assertEq(minter.balance, 0);

    // eth is held at fee manager
    assertEq(address(feeManager).balance, 1 ether - 0.0106 ether);
}

Impact

Any unused fee won't be refunded to the caller and will be left in the FeeManager contract.

Code Snippet

https://github.com/sherlock-audit/2024-04-titles/blob/main/wallflower-contract-v2/src/editions/Edition.sol#L241

https://github.com/sherlock-audit/2024-04-titles/blob/main/wallflower-contract-v2/src/editions/Edition.sol#L267

https://github.com/sherlock-audit/2024-04-titles/blob/main/wallflower-contract-v2/src/editions/Edition.sol#L296

https://github.com/sherlock-audit/2024-04-titles/blob/main/wallflower-contract-v2/src/editions/Edition.sol#L319

Tool used

Manual Review

Recommendation

FeeManager should refund unused ETH to the caller (the Edition in this case) so that the Edition contract can refund the original caller.

Duplicate of #269

realfugazzi commented 5 months ago

Escalate

This is a valid issue, duplicate of #269

sherlock-admin3 commented 5 months ago

Escalate

This is a valid issue, duplicate of #269

You've created a valid escalation!

To remove the escalation from consideration: Delete your comment.

You may delete or edit your escalation comment anytime before the 48-hour escalation window closes. After that, the escalation becomes final.

WangSecurity commented 5 months ago

Agree with the escalation, planning to accept and duplicate with #269

Evert0x commented 5 months ago

Result: High Duplicate of #269

sherlock-admin2 commented 5 months ago

Escalations have been resolved successfully!

Escalation status: