Reach out for an audit or to learn more about Macro
or Message on Telegram

Nouns Stream A-1

Security Audit

May 8, 2023

Version 1.0.0

Presented by 0xMacro

Table of Contents

Introduction

This document includes the results of the security audit for Nouns Stream's smart contract code as found in the section titled ‘Source Code’. The security audit was performed by the Macro security team from April 17, 2023 to April 21, 2023.

The purpose of this audit is to review the source code of certain Nouns Stream Solidity contracts, and provide feedback on the design, architecture, and quality of the source code with an emphasis on validating the correctness and security of the software in its entirety.

Disclaimer: While Macro’s review is comprehensive and has surfaced some changes that should be made to the source code, this audit should not solely be relied upon for security, as no single audit is guaranteed to catch all possible bugs.

Overall Assessment

The following is an aggregation of issues found by the Macro Audit team:

Severity Count Acknowledged Won't Do Addressed
Critical 2 - - 2
High 4 - - 4
Medium 1 - - 1
Low 2 2 - -
Code Quality 13 1 - 12
Informational 1 1 - -

Nouns Stream was quick to respond to these issues.

Specification

Our understanding of the specification was based on the following sources:

Source Code

The following source code was reviewed during the audit:

Specifically, we audited the following contracts within this repository:

Source Code SHA256
app/web3/src/VersionedContract.sol

a11161bc74c88926d44784a86bb7aa6a538b510facf9d87966be2f672743aeea

app/web3/src/intervals/Intervals.sol

8c719bd3e5818322a54a188910f6f7149e7a917576a2690d5a62ae74ccb26529

app/web3/src/intervals/storage/IntervalsStorageV1.sol

06002a887a3687482ef63d8131ec68bbab8764a8cc910b3f9e21f9141c2183e5

app/web3/src/lib/Factory.sol

cde2035978d6bf4de5e915490c09815a3470b95cdba82e188a053f941b787093

app/web3/src/lib/Stream.sol

1e595ab036bc438ee6aa44f31925c3c09c8da014cf33f3620ad67cc062eefdba

app/web3/src/lib/storage/FactoryStorageV1.sol

b188ab8c1ea1dbd7b98d234c950dcd0295d9283de3c8480af918a298b3b885bb

app/web3/src/manager/Manager.sol

ddaae1b8f4276ec8ac1bd295e1e1938a04f8ce1bb25c1ca8ba5f23bfbc0f7ca4

app/web3/src/milestones/Milestones.sol

db42b3cae0f2058de9388f8d796af472d9e6a98f6cf26e5ae0cea1e9dffe76f7

app/web3/src/proxy/ERC1967Proxy.sol

b1d2f27359c9f3237e0cbc091932cdbb0f5178b7abec0de4dd3985c25be9a44c

app/web3/src/proxy/ERC1967Upgrade.sol

d3fd03cd782a6c6c985e6920dd7782d80d4868431eb827b9d63a0f61f963e30f

app/web3/src/proxy/UUPS.sol

0739c8b4ef57db555ed9296f137244d2b9b30e25b1b9a14c4c62cb8d50084a5b

app/web3/src/utils/Address.sol

551c7ab8b97a01e994016373ad161951c03120808b6f34db31ef2a05758cedbb

app/web3/src/utils/Clones.sol

64569dbcb986701b03b3ac4916823190dc48ceaf4af7746d0e048762c077353d

app/web3/src/utils/FullMath.sol

0bc73712566628c5c0fa851f7b2450ae1ef50f83becafaeb0a79e374688ee843

app/web3/src/utils/Initializable.sol

791397428dae4722fce22a6400c6f18eef2af4ab0432449382087204862b5aad

app/web3/src/utils/Ownable.sol

01e43bf261e7b360f90538674907ca4e340f472913d8e7d3d7cbfeee3eb9d603

app/web3/src/utils/Pausable.sol

7a2242e65ef84149351ecbb6ab0cbd832b9fac6a313f97d2fcbdada8f800931a

Note: This document contains an audit solely of the Solidity contracts listed above. Specifically, the audit pertains only to the contracts themselves, and does not pertain to any other programs or scripts, including deployment scripts.

Issue Descriptions and Recommendations

Click on an issue to jump to it, or scroll down to see them all.

Security Level Reference

We quantify issues in three parts:

  1. The high/medium/low/spec-breaking impact of the issue:
    • How bad things can get (for a vulnerability)
    • The significance of an improvement (for a code quality issue)
    • The amount of gas saved (for a gas optimization)
  2. The high/medium/low likelihood of the issue:
    • How likely is the issue to occur (for a vulnerability)
  3. The overall critical/high/medium/low severity of the issue.

This third part – the severity level – is a summary of how much consideration the client should give to fixing the issue. We assign severity according to the table of guidelines below:

Severity Description
(C-x)
Critical

We recommend the client must fix the issue, no matter what, because not fixing would mean significant funds/assets WILL be lost.

(H-x)
High

We recommend the client must address the issue, no matter what, because not fixing would be very bad, or some funds/assets will be lost, or the code’s behavior is against the provided spec.

(M-x)
Medium

We recommend the client to seriously consider fixing the issue, as the implications of not fixing the issue are severe enough to impact the project significantly, albiet not in an existential manner.

(L-x)
Low

The risk is small, unlikely, or may not relevant to the project in a meaningful way.

Whether or not the project wants to develop a fix is up to the goals and needs of the project.

(Q-x)
Code Quality

The issue identified does not pose any obvious risk, but fixing could improve overall code quality, on-chain composability, developer ergonomics, or even certain aspects of protocol design.

(I-x)
Informational

Warnings and things to keep in mind when operating the protocol. No immediate action required.

(G-x)
Gas Optimizations

The presented optimization suggestion would save an amount of gas significant enough, in our opinion, to be worth the development cost of implementing it.

Issue Details

C-1

Anyone can drain funds

Topic
Protocol Design
Status
Impact
High
Likelihood
High

In Stream.sol’s _distribute(), it is intended to be called internally and distribute funds in set amounts to the recipient based on the type of stream and conditions set.

However, _distribute() is a public function with no checks or restrictions on who can call:

function _distribute(address _to, uint256 _amount) public {
    if (token != address(0)) {
        /// ERC20 transfer
        SafeERC20.safeTransfer(IERC20(token), _to, _amount);
    } else {
        /// ETH transfer
        bool success;
        assembly {
            /// Limit the call to 50,000 gas
            success := call(50000, _to, _amount, 0, 0, 0, 0)
        }
    }
}

Reference: Line 77 - 89, Streams.sol

This allows anyone to drain the contract of all its funds.

Remediations to Consider

Set the access of _distribute() to internal.

C-2

Stream deployment can be front-run and set as owner

Topic
Frontrunning
Status
Impact
High
Likelihood
High

A Stream contract is intended to be deployed with a deterministic address, that is generated using various input parameters to generate a salt used to deploy via CREATE2. Users can predict this address and send funds intended to be held in escrow before deploying a Stream.

However, an important parameter missing in the salt generation is the _owner, which is the address that takes ownership of the Stream.sol contract and can call permissioned functions that can pause and withdraw all funds.

There are multiple cases where issues can arise because of this.

  • In the case where a Stream is pre-funded in a separate transaction by predicting the address beforehand, then someone can front-run the Stream deployment with themselves as owner, then immediately withdraw all funds to themselves.
  • In the case where an externally owned account attempts to create a Stream, another malicious actor can front-run the deployment, causing the users’s transaction to revert. This may not be clear to the user, as a contract with an address they expect is deployed. They may mistakenly send funds to it thinking they have ownership, which could then be drained.
  • In the case where a DAO proposal intends to deploy a Stream, and potentially send funds to it, or take other actions described by the proposal. Since the deployment parameters are public and take time to vote on/execute, someone can front-run the deployment and depending on how the DAO handles errors, either revert the whole proposal, or potentially send funds to the Stream contract that it doesn’t own.
  • General griefing can also potentially occur, preventing anyone from properly deploying a Stream contract.

Remediations to Consider

  • Include the _owner parameter in the salt generation, so that the _owner address cannot be swapped out by someone malicious without changing the contract's address.
  • Also, include the address of the deployer(msg.sender) that made the deployment call in the salt. This would prevent any possible griefing by deploying the same address beforehand, as a different address would be generated based on the sender.
H-1

Interval funds locked after endTime

Topic
Spec
Status
Impact
High
Likelihood
High

In Intervals.sol funds are escrowed in the contract and a set amount can be claimed after a given interval; a startTime and endTime of this contract determine the duration and payment intervals for funds being released.

However, if owed funds are claimed or released after the set endTime, then the call to _nextPayment() will revert:

if (block.timestamp > endDate) revert STREAM_FINISHED();

Reference: Line 121, Intervals.sol

This will lock any unclaimed funds in the contract, locking all possible funds owed, and likely to lock the last intervals payment especially if the duration of the contract is divisible by the interval, as claiming would require execution to occur at the exact endTime of the contract, which may not be possible.

Remediations to Consider

Instead of reverting, if block.timestamp is greater than the endDate, set a local time variable to endDate and proceed with the calculation, ensuring that the user will get paid up to the endDate.

H-2

Owner may be prevented from withdrawing

Topic
Protocol Design
Status
Impact
High
Likelihood
Medium

In Stream.sol’s withdraw(), if the contracts payment token is ETH transfer is used to send the funds from the contract to the owner:

if (token == address(0)) {
    bal = address(this).balance;
    payable(owner()).transfer(bal);
}

Reference: Line 32-34, Stream.sol

However, transfer sends a fixed amount of gas, and assumes the address it sends the ETH to doesn’t have a fallback or receive function that runs code. If the owner is a contract with a custom fallback function, like a multi-sig or a DAO, then the transfer will fail, and the owner will be prevented from withdrawing their funds, potentially locking them in the contract.

Remediations to Consider

Use a low-level call to transfer ETH; doing so will allow any contract to be the owner and withdraw without issue.

H-3

Not checking call return value in _distribute

Topic
Spec
Status
Impact
High
Likelihood
Medium

In Stream.sol, the function _distribute transfers a specific amount of assets to an address. Depending on the token the logic will perform an ERC20 transfer or an assembly low-level call to send the assets to the recipient.

function _distribute(address _to, uint256 _amount) public {
    if (token != address(0)) {
                ...
    } else {
        /// ETH transfer
        bool success;
        assembly {
            /// Limit the call to 50,000 gas
            success := call(50000, _to, _amount, 0, 0, 0, 0)
        }
    }
}

Reference: Line 77 - 89, Stream.sol

This function is called in both, Intervals.sol and Milestones.sol contracts in release() and claim() functions.

For ETH payments, if the assembly call fails to send the assets, the transaction will not be reverted, the paid/currentMilestone variables will be updated and the FundsDisbursed event emitted. This could happen if the Stream contract doesn’t have enough funds or if the recipient fails to receive the assets.

Remediations to Consider:

  • Revert the transaction if the call fails or,
  • Using a try-catch to process failed transfers accordingly.
H-4

recipient can claim ETH earlier than intended

Topic
Protocol Design
Status
Impact
High
Likelihood
Low
This issue was found by an external third party and reported to us to verify.

In Milestones.sol and Intervals.sol, the functions claim() and release() distribute the owed tokens or ETH to the recipient.

However, the state that determines the amount to send to the recipient is updated after the internal function _distribute() is called, which will send ETH to the recipient if the token address is set to address(0).

function claim() external payable whenNotPaused returns (uint256) {
    uint256 bal = _nextPayment();
    if (bal == 0) revert NO_FUNDS_TO_DISBURSE();    
    _distribute(recipient, bal);

    currentMilestone++;

    emit FundsDisbursed(address(this), bal, "Milestones");

    return bal;
}

Reference: Line 75-86, Milestones.sol

function _distribute(address _to, uint256 _amount) public {
    if (token != address(0)) {
        /// ERC20 transfer
        SafeERC20.safeTransfer(IERC20(token), _to, _amount);
    } else {
        /// ETH transfer
        bool success;
        assembly {
            /// Limit the call to 50,000 gas
            success := call(50000, _to, _amount, 0, 0, 0, 0)
        }
    }
}

Reference: Line 77-89, Stream.sol

In the case where the recipient is a contract that contains a malicious fallback or receive function that calls either claim() or release() when receiving ETH, it would allow the recipient to drain the entire contract of its funds after only needing to wait a single interval, or complete a single milestone.

Remediations to Consider

Follow the Checks, Effects, and Interactions pattern, and update currentMilestone for Milestones.sol and paid for Intervals.sol, before making the call to _distribute() for both claim() and release().

M-1

receive should revert if token is non-zero

Topic
Protocol Design
Status
Impact
High
Likelihood
Low

A Stream contract intends to receive funds in either ETH or a set ERC20 token based on the token parameter it is initialized with. Based on the value of token, the contract can send out or have that token withdrawn. In the case the contract deals with ETH, token is set to address(0). However, if token is set to a non-zero address of an ERC20 token, the contracts receive() function still allows ETH to be directly sent into it, although it has no means of dealing with it, and it would be locked.

Remediations to Consider

receive() should revert if the set token value is non-zero in order to prevent ETH from being accidentally locked in the contact.

L-1

Lack of initialization parameter sanity checks

Topic
Sanity checks
Status
Acknowledged
Impact
Low
Likelihood
Medium

The manager contract, allows any user to deploy an Interval or Milestone stream contract with a specific set of parameters that will initialize the state variables of the deployed contract.

While initializing these parameters, there are no sanity checks on whether these inputs will be within a functional range. Here is a list detailing sanity checks to ensure no misconfigurations are possible.

Intervals.sol:

  • _interval > 0
  • _interval < _endDate
  • block.timestamp **≤** _startDate < _endDate
  • block.timestamp < _endDate

Milestones.sol:

  • _msPayments.length == _msDate.length
  • _msDates[i] < _msDates[i + 1]
  • _msDates[0] ≥ block.timestamp

Remediations to Consider:

  • Consider adding these sanity checks in the initialize function or,
  • Documenting possible pitfalls on initialization if inputs are not valid.
Response by Nouns Stream

The user interface will validate the parameters, and we will provide an NPM SDK that users must use to verify parameters before creating a stream. We aim to reduce gas costs by incorporating less code into the contracts.

L-2

Non-descriptive events

Topic
Events
Status
Acknowledged
Impact
Low
Likelihood
High

Events should be emitted to describe changes to the state of the contract, and allow off-chain applications to easily understand relevant information as it pertains to its desired functionality.

However, most of the events emitted in the contracts do not give the relevant information that would be useful for applications and users.

In the case of Manager.sol’s StreamCreated event, it contains two parameters that describe the created contracts address, and the stream type. It would, however, be useful if the owner address and the recipient address were present and indexed, as it would allow an application to query the Manager.sol contracts StreamCreated event and filter all streams created for a select address is either the owner or recipient.

In the case of Intervals.sol’s FundsDisbursed event, it contains three parameters that describe the stream contracts address, the amount of funds disbursed, and the stream type. However, the address of the stream contract is not useful as the event is emitted from its address, so anyone listening to this event has to know the address of this stream. The stream type may not be relevant as it is immutable within a given stream. The owner and recipient, however, can change and could be relevant information to emit, as well as either the milestone index or the time interval the payment was for, depending on the stream type.

Remediations to Consider

Refactor events to emit all the relevant info, and index the important parameters to allow an application to easily query and use with minimal friction. Remove any irrelevant information as well, like any instance where the event emits its own contract address.

Response by Nouns Stream

We’ve created a subgraph to index data and make it easy to access events and the data associated with them.

I-1

Escrow assets are not checked during initialization

Topic
Protocol Design
Status
Acknowledged
Impact
Informational

The current design of both stream contracts (Milestones.sol and Intervals.sol) doesn’t perform any checks on the balances of the contracts, neither while initializing the contract nor performing payments.

This could result in stream contracts getting created without enough balance to perform the corresponding payments or even without any balances at all.

Q-1

isUpgrade mapping is not used

Topic
Code Quality
Status
Quality Impact
Low

Factory.sol inherits from FactoryStorageV1.sol to use its isUpgrade mapping. Factory.sol exposes functions that the owner can call to add and remove values in this mapping, as well as check if a value is set.

However, isUpgrade is not currently used and is not involved in the upgradability of the Factory.sol contract.

Remediations to Consider

Remove all references to FactoryStorageV1.sol and its isUpgrade mapping.

Q-2

Unused imports

Topic
Code Quality
Status
Quality Impact
Low

The following files are imported but are unused:

  • Stream.sol: FullMath.sol and IFactory.sol
  • Factory.sol: ERC1967Proxy.sol
  • Milestones.sol: IFactory.sol
  • Intervals.sol: IFactory.sol

IntervalsStorageV1.sol is a duplicate of FactoryStorageV1.sol and unused.

Remediations to Consider

Remove the above unused imports and files.

Q-3

Add missing natSpec documentation

Topic
Code Quality
Status
Quality Impact
Low

Some functions are missing return parameters in their natSpec documentation.

Remediations to Consider

Complete the natSpec documentation for each function to describe the values each returns.

Q-4

Intervals math can be cleaned up

Topic
Code Quality
Status
Quality Impact
Medium

In Intervals.sol’s _nextPayment() some math is done to determine the number of intervals that have passed since the last time the user has claimed or had their funds released:

uint256 elapsed = block.timestamp - paid;
if (elapsed < interval) return (0, 0);

uint256 multiplier = (elapsed - (elapsed % interval)) / interval;

Reference: Line 126-129, Intervals.sol

However, the multiplier calculation can be simplified and made more clear if changed to:

uint256 multiplier = elapsed / interval;

Remediations to Consider

Replace the multiplier calculation with the above suggested line to be more clear about what is being calculated.

Q-5

Unnecessary use of payable

Topic
Code Quality
Status
Quality Impact
Medium

Some functions and constructors contain the payable keyword when it is not necessary. This could cause users to be confused or accidentally send funds into the contract.

Functions with unnecessary payable:

  • Intervals.sol: constructor() and claim()
  • Milestones.sol: claim()
  • Stream.sol: withdraw()
  • Manager.sol: constructor()

Remediations to Consider

Remove the payable keyword from the functions listed above.

Q-6

Unnecessary initialize return value

Topic
Code Quality
Status
Quality Impact
Low

In Intervals.sol and Milestones.sol, their initialize function returns its own address. This is unnecessary as any caller already needed its address to call initialize.

Remediations to Consider

Remove the return value of initialize.

Q-7

tip has no bounds

Topic
Code Quality
Status
Acknowledged
Quality Impact
Medium

In both stream contracts, Intervals.sol and Milestones.sol, the tip variable acts as an incentive to a bot to perform push payments to the recipient. However, this value is not checked against the owed/msPayments amounts.

Remediations to Consider

Consider using a percentage related to the amount owed.

Response by Nouns Stream

The user interface will validate the parameters, and we will provide an NPM SDK that users must use to verify parameters before creating a stream. We aim to reduce gas costs by incorporating less code into the contracts.

Q-8

Initialization of storage variable to default value

Topic
Code Quality
Status
Quality Impact
Low

Milestone.sol’s initialize function stores a zero value to the currentMilestone state variable. However, this is the default value of all variables on deployment.

currentMilestone = 0;

Reference: Line 47, Milestones.sol

Remediations to Consider

Consider removing this variable assignment.

Q-9

Duplicated check in _nextPayment()

Topic
Code Quality
Status
Quality Impact
Low

In Interval.sol, _nextPayment() function we can see the following code:

uint256 nextInterval = paid + interval;
if (block.timestamp < nextInterval) return (0, 0);

uint256 elapsed = block.timestamp - paid;
if (elapsed < interval) return (0, 0);

Reference: Line 123-127, Intervals.sol

Both of these if statements, check the same interval correlation.

Remediations to Consider

Since the first nextInterval variable is not used afterward, consider removing the first two redundant lines.

Q-10

Inconsistent response to zero balance

Topic
Code Quality
Status
Quality Impact
Low

In Interval.sol’s claim and release functions each have similar code returning zero if bal is zero:

(uint256 bal, uint256 multiplier) = _nextPayment();
if (bal == 0) return 0;

Reference: Lines 77-78 and lines 93-94,Intervals.sol

However, in Milestones.sol’s same functions revert in the case where bal is zero:

uint256 bal = _nextPayment();
if (bal == 0) revert NO_FUNDS_TO_DISBURSE();

Reference: Lines 59-60 and lines 76-77, Milestones.sol

Remediations to Consider

Either choose to have both contracts return zero in this case, or have both revert in order to be more consistent.

Q-11

Naming convention

Topic
Code Quality
Status
Quality Impact
Low

Within the contracts, there are variables that may cause confusion given their uses.

Remediations to Consider

Consider renaming the following:

  • Intervals.sol and Milestones.sol release()/claim(): _bal to something like _amount.
  • Intervals.sol: paid to something like _lastTimePaid. (Also consider updating the comment for this variable as it’s not accurate).
  • Manager.sol createIntvStream()/createMSStream(): _owner parameter to something like _streamOwner. To avoid confusion with the internal _owner of Manager.sol contract.
Q-12

Merge Factory.sol with Manager.sol

Topic
Code Quality
Status
Quality Impact
Medium

The Intervals.sol contract initializes itself in its constructor, as it is intended to be a logic contract that is delegated to, and to prevent anyone from calling initialize on it.

However, this is missing in Milestones.sol. There isn’t anything malicious that can be done by calling initialize() on the contract itself, but is good practice and more consistent with how Intervals.sol is implemented.

Remediations to Consider

Initialize Milestones.sol in its constructor. Consider also using _disableInitializers() instead of the initializer modifier, in both Milestones.sol and Intervals.sol’s constructors to be more clear that these implementation contracts cannot be initialized maliciously.

Q-13

Missing initialized constructor in Milestones.sol

Topic
Code Quality
Status
Quality Impact
Medium

The Intervals.sol contract initializes itself in its constructor, as it is intended to be a logic contract that is delegated to, and to prevent anyone from calling initialize on it.

However, this is missing in Milestones.sol. There isn’t anything malicious that can be done by calling initialize() on the contract itself, but is good practice and more consistent with how Intervals.sol is implemented.

Remediations to Consider

Initialize Milestones.sol in its constructor. Consider also using _disableInitializers() instead of the initializer modifier, in both Milestones.sol and Intervals.sol’s constructors to be more clear that these implementation contracts cannot be initialized maliciously.

Disclaimer

Macro makes no warranties, either express, implied, statutory, or otherwise, with respect to the services or deliverables provided in this report, and Macro specifically disclaims all implied warranties of merchantability, fitness for a particular purpose, noninfringement and those arising from a course of dealing, usage or trade with respect thereto, and all such warranties are hereby excluded to the fullest extent permitted by law.

Macro will not be liable for any lost profits, business, contracts, revenue, goodwill, production, anticipated savings, loss of data, or costs of procurement of substitute goods or services or for any claim or demand by any other party. In no event will Macro be liable for consequential, incidental, special, indirect, or exemplary damages arising out of this agreement or any work statement, however caused and (to the fullest extent permitted by law) under any theory of liability (including negligence), even if Macro has been advised of the possibility of such damages.

The scope of this report and review is limited to a review of only the code presented by the Nouns Stream team and only the source code Macro notes as being within the scope of Macro’s review within this report. This report does not include an audit of the deployment scripts used to deploy the Solidity contracts in the repository corresponding to this audit. Specifically, for the avoidance of doubt, this report does not constitute investment advice, is not intended to be relied upon as investment advice, is not an endorsement of this project or team, and it is not a guarantee as to the absolute security of the project. In this report you may through hypertext or other computer links, gain access to websites operated by persons other than Macro. Such hyperlinks are provided for your reference and convenience only, and are the exclusive responsibility of such websites’ owners. You agree that Macro is not responsible for the content or operation of such websites, and that Macro shall have no liability to your or any other person or entity for the use of third party websites. Macro assumes no responsibility for the use of third party software and shall have no liability whatsoever to any person or entity for the accuracy or completeness of any outcome generated by such software.