code-423n4 / 2022-06-putty-findings

5 stars 0 forks source link

`fillOrder` allow the order to have `order.premium == 0` #203

Closed code423n4 closed 2 years ago

code423n4 commented 2 years ago

Lines of code

https://github.com/code-423n4/2022-06-putty/blob/main/contracts/src/PuttyV2.sol#L268-L380

Vulnerability details

Impact

The premium property of an order define the amount of ETH/ERC20 (it depends on order.baseAsset) that the "longer" user has to send to the "shorter" user.

Currently, the contract allow having orders with premium == 0. This mean that the "shorter" will not be paid to provide the option.

Without a premium, the "longer" could use interact with the contract without having anything to lose (they just need to wait for the option to expire).

Proof of Concept

Tools Used

Manual review + forge test. Here is the test to showcase this issue

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import "forge-std/Test.sol";
import "forge-std/console.sol";
import "openzeppelin/utils/cryptography/ECDSA.sol";

import "src/PuttyV2.sol";
import "./shared/Fixture.t.sol";

contract TestC4ZeroPremium is Fixture {
    event WithdrawOrder(bytes32 indexed orderHash, PuttyV2.Order order);

    address[] internal floorTokens;
    PuttyV2.ERC20Asset[] internal erc20Assets;
    PuttyV2.ERC721Asset[] internal erc721Assets;
    uint256[] internal floorAssetTokenIds;

    receive() external payable {}

    function setUp() public {
        deal(address(weth), address(this), 0xffffffff);
        deal(address(weth), babe, 0xffffffff);

        weth.approve(address(p), type(uint256).max);

        vm.prank(babe);
        weth.approve(address(p), type(uint256).max);
    }

    function testItSendsZeroPremiumWETHToMakerIfShort() public {
        // arrange
        PuttyV2.Order memory order = defaultOrder();
        order.premium = 0;
        order.isLong = false;
        bytes memory signature = signOrder(babePrivateKey, order);

        uint256 takerBalanceBefore = weth.balanceOf(address(this));
        uint256 makerBalanceBefore = weth.balanceOf(order.maker);

        // act
        p.fillOrder(order, signature, floorAssetTokenIds);

        // assert
        assertEq(weth.balanceOf(order.maker) - makerBalanceBefore, 0, "Should have transferred premium to maker");

        assertEq(takerBalanceBefore - weth.balanceOf(address(this)), 0, "Should have transferred premium from taker");
    }

    function testItSendsZeroPremiumERC20ToMakerIfShort() public {
        // arrange
        PuttyV2.Order memory order = defaultOrder();
        order.baseAsset = address(link);
        order.premium = 0;
        order.isLong = false;
        bytes memory signature = signOrder(babePrivateKey, order);

        uint256 takerBalanceBefore = link.balanceOf(address(this));
        uint256 makerBalanceBefore = link.balanceOf(order.maker);

        // act
        p.fillOrder(order, signature, floorAssetTokenIds);

        // assert
        assertEq(link.balanceOf(order.maker) - makerBalanceBefore, 0, "Should have transferred premium to maker");

        assertEq(takerBalanceBefore - link.balanceOf(address(this)), 0, "Should have transferred premium from taker");
    }

    function testItSendsZeroPremiumWETHToTakerIfLong() public {
        // arrange
        PuttyV2.Order memory order = defaultOrder();
        order.premium = 0;
        order.isLong = true;
        bytes memory signature = signOrder(babePrivateKey, order);

        uint256 takerBalanceBefore = weth.balanceOf(address(this));
        uint256 makerBalanceBefore = weth.balanceOf(order.maker);

        // act
        p.fillOrder(order, signature, floorAssetTokenIds);

        // assert
        assertEq(weth.balanceOf(address(this)) - takerBalanceBefore, 0, "Should have transferred premium to taker");

        assertEq(makerBalanceBefore - weth.balanceOf(order.maker), 0, "Should have transferred premium from maker");
    }

    function testItSendsZeroPremiumERC20ToTakerIfLong() public {
        // arrange
        PuttyV2.Order memory order = defaultOrder();
        order.baseAsset = address(link);
        order.premium = 0;
        order.isLong = true;
        bytes memory signature = signOrder(babePrivateKey, order);

        uint256 takerBalanceBefore = link.balanceOf(address(this));
        uint256 makerBalanceBefore = link.balanceOf(order.maker);

        // act
        p.fillOrder(order, signature, floorAssetTokenIds);

        // assert
        assertEq(link.balanceOf(address(this)) - takerBalanceBefore, 0, "Should have transferred premium to taker");

        assertEq(makerBalanceBefore - link.balanceOf(order.maker), 0, "Should have transferred premium from maker");
    }
}

Recommended Mitigation Steps

Prevent the user to create off-chain orders with premium == 0.

Consider adding a check on fillOder that will revert if order.premium == 0.

outdoteth commented 2 years ago

Duplicate: Possible to create spam orders with 0 strike and premium: https://github.com/code-423n4/2022-06-putty-findings/issues/305

HickupHH3 commented 2 years ago

Part of warden's QA: #199