Nouns Currents

One-liner - Update the Nouns streams contract to increase streams and propdates use, and promote accountability in Nouns.

  • Problem: A majority of proposals do not use streams and do not post updates. In order to increase accountability in the DAO and proposal quality, we need to increase usage of Streams and Propdates.

  • Solution: Revamp the current streams contract, which was initially just a Sablier fork, to include the following features:

    • Stream as an NFT: transferable, more secure in case of needing to transfer wallets, and makes Proof-of-Contribution more legible onchain. Stores all data in the NFT contract and renders it onchain.
    • Update Requirement: in order to withdraw funds from a stream, the owner must post an update if one hasn’t been posted in the last 30 days (this duration is configurable by the DAO)
    • Payment Strategies: depending on a proposal’s needs, a stream can be linear, cliff-based (X ETH every Y days), or exponential (slow at first, steadily increasing, for high risk proposals).

Currents builds on top of the current Nouns Streams contracts, as well as previous work by both Sablier v2 and Zora’s Faucets.

By investing in a new stream primitive that fits contributor’s needs, we can increase the number of proposals that use streams and post regular updates. This improves transparency across the DAO and increases our odds of spotting bad actors and canceling their payments.

Below is a diagram and code sample of how the contracts might work.

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

// SPDX-License-Identifier: GPL-3.0

pragma solidity ^0.8.17;

contract Propstream is ERC721, IStrategy{

    struct CurrentInfo {
        uint256 tokenAmount,
        address tokenAddress,
        uint256 startTime,
        uint256 stopTime,
        uint256 propId,
        IStrategy strategy
    }

 /**
 * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
 *   WRITE
 * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
 */

    /**
     * @notice mint a token to a recipient and initiate the stream.
     * Intended to be set via proposal transaction
	 * Can only be called via DAO executor address
     * @param to the address to mint the NFT to.
     * @param tokenAmount the amount of token to stream.
     * @param tokenAddress the address of the token to stream.
     * @param startTime the timestamp the stream should start.
     * @param propId the propID the nft should have update access to.
     * @param propUpdateAdmin bool for if this NFT can post proposal updates.
     * @param strategy the type of stream this prop should allow (Lumpsum vs Linear). Would be a contract address that, when called by the claim function, performs a check to see how much available balance is claimable.
     */
    function createCurrent(
        address to,
        uint256 tokenAmount,
        address tokenAddress,
        uint256 startTime,
        uint256 stopTime,
        uint256 propId,
        bool propUpdateAdmin,
        IStrategy strategy
    ) external payable {
        // mint NFT
        // assign storage mapping to NFT
        // transfer ERC20 amount
    }
    
    
    /**
     * @notice deposit tokens for a minted Stream NFT
     * Necessary for async operations where stream can't be funded and created in one transaction
     * @param tokenId the tokenID that this deposit goes to
     * @param tokenAmount the amount of token to deposit
     * @tokenAddress the address of the token to deposit
     */
    function deposit(
        uint tokenId,
        uint256 tokenAmoun,
        address tokenAddress
    ) {
        // require tokenId to exist 
        // require tokenAmount/address to equal stream params
    }
    
    

    /**
     * @notice mint a token to a recipient and initiate the stream.
     * Intended to be done by proposer of a past proposal, or superAdmin
     * Proposer can only mint for a proposal they proposed
     * @param to the address to mint the NFT to.
     * @param tokenAmount the amount of token to stream.
     * @param tokenAddress the address of the token to stream.
     * @param startTime the timestamp the stream should start.
     * @param propId the propID the nft should have update access to.
     * @param propUpdateAdmin bool for if this NFT can post proposal updates.
     * @param strategy the type of stream this prop should allow (Lumpsum vs Linear).
     */
    function recoverCurrent(
        address to,
        uint256 tokenAmount,
        address tokenAddress,
        uint256 startTime,
        uint256 stopTime,
        uint256 propId,
        bool propUpdateAdmin,
        IStrategy strategy
    ) external payable {
        // mint NFT, done by either proposer or proposal
        // assign storage mapping of stream state to NFT
        // transfer ERC20 amount
    }

    /**
     * @notice Withdraw tokens to recipient's account.
     * @notice forked from current stream contract
     * Execution fails if the requested amount is greater than recipient's withdrawable balance.
     * Only this stream's payer or recipient can call this function.
     * @param amount the amount of tokens to withdraw.
     */
    function claimFromActiveBalance(
            uint256 id, 
            uint256 amount
    ) public onlyPayerOrRecipient {
        // call stream strategy contract to return available balance to claim
    }

    /**
     * @notice Cancel the stream and update recipient's fair share of the funds to their current balance.
     * Each party must take additional action to withdraw their funds:
     * recipient must call `withdrawAfterCancel`.
     * payer must call `recoverTokens`.
     * Only this stream's payer or recipient can call this function.
     * Reverts if executed after recipient has withdrawn the full stream amount, or if executed more than once.
     */
    function cancel(
	uint256 id
    ) external onlyPayerOrRecipient {
	// handle logic for canceling stream
    }

    /**
     * @notice Withdraw tokens to recipient's account after the stream has been cancelled.
     * Execution fails if the requested amount is greater than recipient's withdrawable balance.
     * Only this stream's payer or recipient can call this function.
     * @param amount the amount of tokens to withdraw.
     */
    function claimAfterCancel(
            uint256 id, 
            uint256 amount
    ) public onlyPayerOrRecipient {
        // handle logic for withdrawing after cancelation
	// this should limit the amount they can withdraw to the point at cancel
    }

    /**
     * @notice Withdraw tokens to recipient's account. Works for both active and cancelled streams.
     * @param amount the amount of tokens to withdraw
     * @dev reverts if msg.sender is not the payer or the recipient
     */
    function claim(
            uint256 id, 
            uint256 amount
    ) external {
        // call withdrawFromActiveBalance or withdrawAfterCancel
        // depending on stream state
    }

    /**
     * @notice Recover maximumal amount of payment by `payer`
     * This can be used after canceling a stream to withdraw all the unvested tokens
     * @dev Reverts when msg.sender is not this stream's payer
     * @param to the address to send the tokens to
     * @return tokensToWithdraw the amount of tokens withdrawn
     */
    function recoverTokens(
            uint256 id, 
            address to
    ) external returns () {
        // handle logic for recovering tokens from a cancelled stream
    }

    /**
     * @notice Posts an update for a prop
     * Can only be done if propId lastUpdated is within the last n days
     * @param propId The id of the prop
     * @param isCompleted Whether the primary work of the prop is considered done
     * @param update A string describing the update
     */
    function postUpdate(
            uint256 propId, 
            bool isCompleted, 
            string calldata update
    ) external {
            // postUpdate logic - can be done by NFT owner or proposer 
            // will mint a token to the msg.sender if they don't own one
    }

 /**
 * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
 *   VIEW
 * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
 */
		
    /**
     * @notice returns stream data for a given Propstream NFT
     * @param id the tokenID of the Propstream NFT
     */
    function streamInfo(
            uint256 tokenId
    ) public pure returns (StreamInfo memory) {
	    // return streamInfo for tokenID
    }
		
    /**
     * @notice fetches propdate status for a given prop
     * @param propId the id of the prop
    */
    function propdateInfo(
            uint256 propId
    ) external view returns (PropdateInfo memory) {
        // return propdateInfo for propId
    }

    /**
     * @notice returns tokenURI for NFT from the external renderer contract
     * @param tokenId
    */
    function tokenURI(
            uint256 tokenId
    ) external view returns {
            // return tokenURI from renderer contract
    }

 /**
 * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
 *   ADMIN
 * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
 */

    /**
     * @notice Withdraw ETH sent to this contract.
     * Reverts if ETH sending failed.
     * @param to the address to send ETH to.
     * @param amount the amount of ETH to recover.
     */
    function withdraw(
            address to, 
            uint256 amount
    ) external onlyPayer {
        // withdraw any excess ETH in the contract
    }

     /**
     * @notice Sets a new superAdmin, with authority to mint tokens for old proposals
     * @param newSuperAdmin the new superAdmin
     */
    function setSuperAdmin(
            address newSuperAdmin
    ) external onlySuperAdmin {
            // set new superAdmin
    }

    /**
     * @notice Sets a new metadata renderer for the NFT contract
     * @param newRenderer the new metadata renderer contract
     */
    function setRenderer(
            address newRenderer
    ) external onlySuperAdmin {
            // set new superAdmin
    }
		
    /**
     * @notice set the amount of days prior that an update must exist to claim funds
     * @param days the amount of days
     */
    function setUpdateBuffer(
            uint days
    ) external onlySuperAdmin {
         // set new update buffer 
    }

}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";

interface IStrategy is IERC165 {

    /// @notice returns the amount eligible at current timestamp for a Currents tokenID
    /// Reads from Currents token state to calculate claimable amount
    /// @param tokenID the tokenID for the Currents NFT
    function claimableAmount(
        uint256 tokenID
    ) external view returns (uint256);
}

Questions

  • There's no way to enforce onchain that an the propID passed in the mint function reflects the propID of the prop. Currently, we'd be relying on UX from clients as well as social incentives of the DAO to call out any mistakes. How large of a concern is this?