Skip to content

Latest commit

 

History

History
171 lines (133 loc) · 7.61 KB

006.md

File metadata and controls

171 lines (133 loc) · 7.61 KB

Calm Sky Hippo

Medium

Missing access control on updatePrice function leading to DoS attacks, manipulation attempts, or unnecessary costs to the contract owner

Summary

The updatePrice function in the PythOracle contract is callable by any address without restrictions. This absence of access control allows unauthorized users to trigger price updates, leading to denial-of-service (DoS) attacks, manipulation attempts, or unnecessary costs to the contract owner.

Root Cause

The updatePrice function allows any user to update the oracle's price by providing price update data and paying the required fee. However, there are no restrictions on who can call this function, leaving it exposed to abuse. Unauthorized actors could:

  1. Call updatePrice repeatedly with incorrect or invalid data, incurring unnecessary costs for the oracle.
  2. Spam the function, leading to a DoS scenario where legitimate users or maintainers cannot update the price. https://github.com/sherlock-audit/2024-11-oku/blob/main/oku-custom-order-types/contracts/oracle/External/PythOracle.sol#L35-L49
function updatePrice(
    bytes[] calldata priceUpdate
) external payable override returns (uint256 updatedPrice) {
    uint fee = pythOracle.getUpdateFee(priceUpdate);
    pythOracle.updatePriceFeeds{value: fee}(priceUpdate);

    IPyth.Price memory price = pythOracle.getPriceNoOlderThan(
        tokenId,
        uint256(uint64(noOlderThan))
    );
    updatedPrice = uint256(uint64(price.price));
}

Internal pre-conditions

Contract is deployed with pythOracle configured. A malicious actor discovers the updatePrice function has no access control.

External pre-conditions

No response

Attack Path

  1. The attacker repeatedly calls updatePrice with malformed or redundant priceUpdate data.
  2. The attacker wastes contract owner funds or blocks legitimate updates.

Impact

  1. Any malicious user can waste fees, leading to financial losses for the contract owner or maintainers. While Optimism offers lower transaction fees than Ethereum Layer 1, transactions still have a cost. A DoS attacker could: Repeatedly call the vulnerable function (e.g., updatePrice) with meaningless or spam data. Force the legitimate users or the contract owner to pay for on-chain interactions and gas fees over time. Although fees on Optimism are cheaper, they're not free, and cumulative costs can still be significant. A smart contract depends on external services (Pyth) for updates, repeated DoS attacks could: Exhaust resources or rate limits on the oracle infrastructure. Create invalid or redundant data being pushed to the contract. Optimism relies on communication between Layer 2 (Optimism) and Layer 1 (Ethereum). A DoS on Layer 2 could: Increase the cost of resolving issues by requiring emergency measures or interactions with Layer 1, which are more expensive. If the oracle charges fees for updates (e.g., per price update), the contract owner or ecosystem could indirectly pay for frequent updates, especially if funds are allocated for this purpose (e.g., from protocol fees). If the contract uses a pre-funded balance with the oracle service, spammed updates could drain these funds faster than intended.

  2. Denial-of-service (DoS) attacks can disrupt legitimate updates. A malicious user could spam the function with meaningless updates, consuming blockchain resources. Even though the attacker pays the fee, the spam could make it difficult for legitimate users to interact with the oracle due to network congestion. Optimism is a rollup that batches transactions to Ethereum Layer 1. If a malicious actor spams a vulnerable contract, they could: Increase the rollup's batch size, leading to network congestion. Delay processing times for other contracts or users. Each transaction executed in Layer 2 contributes to batch submission costs on Ethereum Layer 1. If the contract is spammed, the rollup's finalization cost increases, which might be passed down to protocols or users. The attacker might "pay" the immediate Layer 2 fees, but the protocol or ecosystem indirectly bears the cost of storing and finalizing those transactions on Layer 1.

  3. Trust and market impact Repeated, unnecessary price updates could lead to perceived unreliability in the price data. Protocols relying on this contract might: Delay critical operations. Mistrust its accuracy or performance, leading to reduced adoption. Trust erosion can have financial implications for protocols built around the contract. If the price data is spammed or manipulated (e.g., due to a lack of timely updates), it could result in cascading effects across DeFi applications (e.g., liquidations in lending protocols or imbalanced AMM pools).

PoC

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

describe("Missing Access Control PoC", function () {
    let PythOracle, pythOracle, mockPyth, tokenId;
    let attacker, owner;

    beforeEach(async () => {
        // Get test accounts
        [owner, attacker] = await ethers.getSigners();

        // Deploy a mock IPyth contract
        const MockPyth = await ethers.getContractFactory("MockPyth");
        mockPyth = await MockPyth.deploy();
        await mockPyth.deployed();

        // Deploy the vulnerable contract
        PythOracle = await ethers.getContractFactory("PythOracle");
        tokenId = ethers.utils.formatBytes32String("TOKEN_ID");
        pythOracle = await PythOracle.deploy(mockPyth.address, tokenId);
        await pythOracle.deployed();
    });

    it("Allows unauthorized attacker to call updatePrice", async function () {
        // Prepare mock data for the price update
        const mockPriceUpdate = ["0x1234"]; // Simulated price update payload
        const mockFee = ethers.utils.parseEther("0.01");

        // Set the update fee in the mock oracle
        await mockPyth.setUpdateFee(mockFee);

        // Attacker attempts to call updatePrice
        await expect(
            pythOracle
                .connect(attacker)
                .updatePrice(mockPriceUpdate, { value: mockFee })
        ).to.not.be.reverted;

        // Verify the mock oracle's updatePriceFeeds was called
        const lastCaller = await mockPyth.lastCaller();
        expect(lastCaller).to.equal(attacker.address);

        console.log("Attacker successfully called updatePrice!");
    });
});

Deploy a mock oracle contract to simulate the behavior of pythOracle:

pragma solidity ^0.8.9;

contract MockPyth {
    uint256 public updateFee;
    address public lastCaller;

    function setUpdateFee(uint256 _fee) external {
        updateFee = _fee;
    }

    function getUpdateFee(bytes[] calldata) external view returns (uint256) {
        return updateFee;
    }

    function updatePriceFeeds(bytes[] calldata) external payable {
        require(msg.value >= updateFee, "Insufficient fee");
        lastCaller = msg.sender; // Log the caller
    }
}

Mitigation

Restrict access to the updatePrice function by allowing only authorized entities to call it. You can use OpenZeppelin's Ownable contract or a role-based access control (RBAC) mechanism.

import "@openzeppelin/contracts/access/Ownable.sol";

contract PythOracle is IPythRelay, Ownable {
    ...

    function updatePrice(
        bytes[] calldata priceUpdate
    ) external payable override onlyOwner returns (uint256 updatedPrice) {
        uint fee = pythOracle.getUpdateFee(priceUpdate);
        pythOracle.updatePriceFeeds{value: fee}(priceUpdate);

        IPyth.Price memory price = pythOracle.getPriceNoOlderThan(
            tokenId,
            uint256(uint64(noOlderThan))
        );
        updatedPrice = uint256(uint64(price.price));
    }
}