Payment Streamer Audit

Summary

Timeline: From 2025-05-12 To 2025-05-14

Total Issues: 5 (3 resolved)
Low Severity Issues: 3 (2 resolved)
Notes & Additional Information: 2 (1 resolved)

Scope

OpenZeppelin reviewed pull request #985 of the compound-finance/comet repository at commit d560079. In scope was the Streamer contract.

System Overview

The Streamer contract is a time-based token distribution mechanism developed by the WOOF! team to continuously stream a fixed USDC-equivalent value of COMP — specifically 2 million USDC — over a one-year period to a designated receiver. Instead of sending USDC directly, the contract settles payouts in COMP tokens, with the required amount being dynamically calculated using Chainlink price feeds at the time of each claim. This allows Compound to fulfill USD-pegged obligations using treasury-held COMP.

Importantly, all obligations are denominated in USDC units, not USD. As a result, if USDC deviates from its USD peg, the real-world value of the stream changes accordingly, while the contract’s logic remains unaffected.

The designated receiver can claim COMP tokens at any time throughout the stream. If the receiver is inactive for more than 7 days, a fallback mechanism allows any address to trigger a claim on their behalf, ensuring liveness of the stream. Compound governance, via its timelock contract, is responsible for funding and initializing the stream. Governance also retains the authority to terminate the stream early by sweeping the remaining COMP balance through a governance proposal.

The stream accrues at a fixed rate of approximately 5,479 USDC per day (2,000,000 USDC over 365 days). While a governance proposal to sweep funds may be submitted at any time, the receiver can continue to claim funds normally throughout the proposal delay and execution window.

Once the stream has ended — after a 365-day duration plus a 10-day buffer — any remaining COMP tokens can be swept by any external caller to the Compound Comptroller. The system is intentionally minimal: it supports no upgrades, parameter reconfiguration, or partial withdrawals, ensuring predictability and clear trust boundaries.

Security Model and Trust Assumptions

The Streamer contract is designed to guarantee predictable and immutable token streaming behavior under well-defined conditions. It enforces strict access controls, time-based value accrual, and oracle-based pricing to ensure that the receiver is paid the correct USDC-equivalent amount in COMP tokens over a fixed one-year period. The system favors minimalism and determinism, removing configuration, pausing, or upgradeability capabilities after deployment.

Security Model

The contract enforces the following key security properties:

  • Linear value accrual: A fixed USDC-equivalent value accrues linearly over time, proportional to elapsed seconds, and never exceeds the total cap of 2,000,000 USDC.
  • Oracle-based pricing: Accurate and fair price conversion between USDC and COMP is enforced using Chainlink price feeds, with a 0.5% slippage buffer to mitigate short-term volatility.
  • Fixed upfront funding: The contract must be pre-funded with enough COMP tokens before initialization. Initialization by the COMPOUND_TIMELOCK will only succeed if the current COMP balance is sufficient to cover the full stream value, adjusted for slippage.
  • Governance-controlled sweeping: The COMPOUND_TIMELOCK can call the sweepRemaining function at any time to withdraw the entire remaining COMP balance. This serves as an emergency mechanism but cannot selectively withdraw partial funds.
  • Receiver claim mechanics: The receiver can claim funds at any time. If inactive for 7 consecutive days, anyone may trigger a claim on their behalf, but funds are always sent to the receiver. After the stream ends, any remaining owed value becomes fully claimable.
  • Graceful underfunding fallback: If the contract lacks sufficient COMP to cover a full claim, the receiver is sent the full remaining COMP balance, and the internal accounting adjusts to reflect the USDC-equivalent value streamed.
  • Post-stream clawback: After the 365-day stream and a 10-day buffer period, anyone can call the sweepRemaining function to return unclaimed funds to the Compound Comptroller, even if the receiver has not claimed the full value.
  • Treasury slippage cost: Due to the price discount applied when converting USDC to COMP, the treasury pays out approximately 100.5% of the nominal USDC stream value (in COMP), absorbing slippage as a protocol cost. This represents an additional slippage cost of over 10,000 USDC value (in COMP) incurred by the treasury, in excess of the 2,000,000 USDC allocation.
  • Immutable logic: The contract cannot be paused, upgraded, or reconfigured after deployment. All parameters and roles are immutable, ensuring a fixed behavior profile.

Trust Assumptions

Although the contract logic is deterministic and self-contained, its correct functioning relies on several external assumptions that are not enforced on-chain:

  • Chainlink Oracle Integrity, Availability, and Immutability: The contract relies on two Chainlink price feeds (COMP/USD and USDC/USD) to convert the USDC-denominated stream into COMP payouts. It assumes that these feeds remain live, authorized, and callable throughout the lifetime of the stream. Specifically, the contract assumes that calls to the latestRoundData function will not revert. If a feed is deprecated, misconfigured, or has become uncallable, claims will revert until the issue is resolved off-chain. The returned prices are also assumed to be accurate, up-to-date, and resistant to manipulation.

    In addition, the system assumes that the oracles’ interfaces and behavior remain immutable. The contract reads the decimal precision from each oracle once during construction and stores it as an immutable value. If an oracle is later upgraded or replaced with a version that returns a different number of decimals or modifies the expected behavior of latestRoundData, the contract will not detect the change, potentially leading to incorrect payout calculations. The system, therefore, relies on both the stability of the data and the consistency of the oracle contract interfaces for the entire duration of the stream.

  • Operational Coordination: The contract requires that it be pre-funded with a sufficient COMP balance before the initialize function is called by the timelock. While initialization includes an on-chain check to verify funding adequacy, the act of transferring COMP and triggering initialize must be coordinated off-chain and executed through a governance proposal. If initialize is called prematurely, it will revert and leave the stream inactive. This setup assumes that the governance or treasury operators will correctly sequence funding and activation.

  • COMP Token Exclusivity: The contract only supports COMP tokens as the streaming asset. No mechanism exists to recover or utilize other ERC-20 tokens that may be sent to the contract by mistake. Any non-COMP tokens transferred to the contract are unrecoverable and will be permanently stuck.

  • Market Price Volatility of COMP: Although the contract verifies that the initial COMP balance is sufficient to cover the full USDC-denominated stream at the time of initialization, it makes no guarantees about future COMP price stability. If the price of COMP drops significantly over the course of the stream, additional top-ups may be required to fulfill the full stream obligation. The contract itself does not enforce or facilitate such top-ups, which is left to the discretion of off-chain governance.

Privileged Roles

The contract defines two privileged roles with clearly scoped authority. All roles and permissions are immutable once the contract is deployed.

  • COMPOUND_TIMELOCK: It represents Compound governance and is the only entity authorized to initialize the contract and activate the stream. This can only occur once and requires the contract to be pre-funded with sufficient COMP to satisfy the full USDC-denominated stream obligation.

    The timelock is also the only party allowed to call sweepRemaining during the active streaming period, which transfers the entire remaining COMP balance to the Comptroller and terminates the stream. Partial withdrawals are not supported, and no other configuration or administrative controls are available to governance once the stream has been initialized.

  • receiver: This address is the designated beneficiary of the stream and the sole recipient of COMP payouts. It may call claim at any time to receive accrued funds, which are calculated based on the elapsed portion of the stream and current oracle pricing. If the receiver is inactive for more than 7 consecutive days, any address may call claim on their behalf, but the funds are still sent directly to the receiver. The receiver role is immutable and cannot be reassigned after deployment.

Low Severity

Misleading Documentation

In the Streamer contract, multiple instances of inaccurate and potentially misleading documentation were identified:

  • The suppliedAmount variable tracks the total USDC-equivalent value that has been streamed to the receiver based on oracle pricing, not the amount supplied to the contract.
  • The sweepRemaining function includes a comment stating that the remaining balance is transferred to the Compound timelock, whereas, the COMP tokens are, in fact, sent to the Comptroller.
  • The getAmountOwed function calculates the total USDC-equivalent value accrued since the stream began and subtracts the amount already streamed (suppliedAmount). However, its comment incorrectly suggests it computes the amount owed based on the time elapsed since the last claim.

Consider updating the aforementioned comments to accurately reflect the contract’s logic and behavior.

Update: Resolved in commit 8eca692f.

Hardcoded Configuration Parameters

The Streamer contract hardcodes the total stream value as 2_000_000e6 USDC via the STREAM_AMOUNT constant. In addition, the STREAM_DURATION is hardcoded to 365 days. These values cannot be changed or configured at deployment or initialization, even though they are core economic parameters of the stream.

As a result, adjusting the stream size or duration - whether for future budget changes, multiple recipients, or extended use cases - would require redeploying a different contract. This inflexibility may limit governance’s ability to adapt the stream to future needs or conditions, despite the fact that the timelock is the only party authorized to initialize the contract.

Consider allowing the stream amount and duration to be passed as immutable or storage variables during initialization so that they can be set by governance through the proposal that initializes the stream.

Update: Acknowledged, not resolved. The WOOF! team stated:

It is an intended approach, as the contract is designed to be a one-time solution for a specific stream, so we want to highlight that by setting all parameters in constants. We believe that any changes in the stream should be processed as a separate proposal.

Missing Documentation

The calculateUsdcAmount function lacks documentation, unlike the similar calculateCompAmount function. While its purpose is reasonably clear by symmetry and naming, documenting it consistently would improve clarity and completeness.

Consider thoroughly documenting the calculateUsdcAmount function. When writing docstrings, consider following the Ethereum Natural Specification Format (NatSpec).

Update: Resolved in commit b60b3de8.

Notes & Additional Information

Unreachable Branch in scaleAmount Function

The scaleAmount function includes a conditional branch for the case where fromDecimals > toDecimals, but this branch is unreachable given the current usage in the contract. The function is always called with toDecimals = 18, and fromDecimals is either the hardcoded value 6 (for USDC) or the immutable value compOracleDecimals, which is set to 8 at construction time based on the Chainlink oracle. As a result, fromDecimals > toDecimals never occurs.

Consider simplifying the scaleAmount function by removing the unreachable branch and renaming or documenting the function to reflect its actual usage. This would improve readability and reduce ambiguity for future reviewers by making the intended behavior and constraints explicit.

Update: Acknowledged, not resolved. The WOOF! team stated:

Since there is no impact on the logic, we want to leave it as it is for code readability.

Unused Named Return Variable

Named return variables are a way to declare variables that are meant to be used within a function’s body for the purpose of being returned as that function’s output. They are an alternative to explicit in-line return statements.

In the getAmountOwed function the owed return variable is unused.

Consider either using or removing any unused named return variables.

Update: Resolved in commit 1a4acd73.

Conclusion

The audited codebase implements a time-based COMP distribution mechanism that streams a fixed USDC-equivalent value to a designated recipient over the course of one year. The system uses Chainlink price feeds to dynamically convert the USDC-denominated obligation into COMP payouts, allowing Compound to fulfill stable-value commitments using protocol-owned assets. The Streamer contract is well-written, minimal in design, and enforces strict access controls and predictable value accrual logic. No critical- or high-severity vulnerabilities were identified.

The overall architecture was found to be robust, with clearly defined roles, immutable parameters, and no upgrade or configuration surface after deployment. The contract assumes correct operational sequencing and oracle availability but otherwise operates autonomously and deterministically. Notably, Compound governance has no control over the 2 million USDC obligation or its emission rate once the stream is initialized. However, the timelock retains the ability to claw back all remaining funds at any time via a governance proposal.

We appreciate the WOOF! team’s responsiveness and clarity throughout the audit process. Their commitment to simplicity and auditability greatly facilitated the review.

2 Likes