Treasury Payment Stream Contract

Link To Source Code: payment-stream - GitHub
Link To Contract Deployer: Create New Payment Stream - Arbiscan

Overview

This forum post describes how to use a payment-stream smart contract alongside the Livepeer Treasury to facilitate time-released payments from the on-chain Treasury. This approach provides an alternative to the default lump sum payment method with a more suitable gradual fund release, all while ensuring that custody of the funds remains decentralized.

How It Works

After a proposal is successfully voted on, funds are transferred from the Treasury into a smart contract. Two guardians are appointed with the authority to terminate the contract and refund any unspent funds back to the Treasury if necessary.

The payee can gradually claim the available tokens on a linear scale as the contract period nears its end. At any point, the payee can access and claim the currently available tokens by invoking the claim() function.

Why Stream Payments?

Accountability: The individual or entity receiving the funds will have to meet their commitments on an on-going basis to continue receiving funds. If these obligations are not met, the guardians have the authority to return the funds to the Treasury.

Voter fatigue: Reduces voter fatigue by bundling many phases, milestone or commitments into one proposal to vote on.

Non-custodial: Funds are securely locked in a contract that ensures payment either to the payee or back to the treasury, with no risk of the funds being lost or stolen.

Improved Cash Flow: Recipients benefit from a steady flow of funds, aiding in better financial planning and management.

Price impact: Streaming payments can mitigate the price impact on LPT tokens that might occur if a lump sum were sold into the market.

Deploying The Contract

Link To Contract Deployer: Create New Payment Stream - Arbiscan

The author of the Treasury proposal would deploy a payment stream contract with the link above and include the parameters needed for the proposal. For any Livepeer Treasury proposals you will need the following information:

Livepeer Treasury Address - 0xf82C1FF415F1fCf582554fDba790E27019c8E8C4

LPT token address - 0x289ba1701C2F088cf0faf8B3705246331cB8A839

You will then fill in the appropriate fields with your information.

Once your contract has been deployed, you can view the contract by viewing the tx hash.

Select “Logs” and locate the payment stream contract address.

Then you can select the “Contract” tab where payees can claim the current available tokens and guardians can submit their vote to terminate the contract to return all funds to the Treasury.

During the Treasury proposal please use the payment stream contract that was created NOT the payment stream deployer address. Be sure to match the LPT funding amount from the contract (18 decimal points) with the standard LPT unit amount in the proposal (1 decimal point). Any over funding will require guardians to terminate the contract to return excess LPT to the Treasury.

That is how the payment stream contract is created :slight_smile:

Other Options

While this is just one option to achieve a payment stream, there are other options such as SuperFluid, Sablier, or modifying OpenZeplins vesting contract to achieve a similar result. Based on my initial research, both Superfluid and Sablier require their contracts to allow token spend from the Treasury, a mechanism I’m not entirely sure if the Treasury can accomplish. Meanwhile, OpenZeppelin’s vesting contracts are highly secure but require rewriting some code to prevent the guardians from having the ability to steal funds. The reason for building a contract from scratch was to accommodate how the current Treasury operates.

Conclusion

I am eager to see our on-chain Treasury thrive and become more robust through these efforts. I welcome any projects interested in utilizing this payment method. If you require further details about this approach or need help setting up the smart contract for your proposal, please don’t hesitate to contact me.

Titan

4 Likes

All for a contract that lets the treasury fund projects over a duration vs dumping potentially millions on their heads and hoping for the best :smile:

I know the contract has been audited once and is really short, but i’d be curious to know if the community would want a second audit to double check everything. I can have a someone take a quick look too :+1:

3 Likes

fanteasy is going to be the first adopter of this contract, really cool!

1 Like

Hi Titan,
Just a question about Claims:
Do they have to be approved by guardians? This would ensure more accountability and reporting, it would stop claims going through without any achievements.
Do claims have set timeframes, and does the receiver need to prove milestones achieved and explain why it was not achieved?

1 Like

No, claiming the available funds does not need to be approved by guardians with the current implementation. And I think adding that feature would add a lot of friction for cashflow to the payee and add too much burden on the guardian.

I view the guardian role as a “pull the trigger” kind of role. If a proposal is failing to achieve milestones, it will likely be seen by many community members and brought up in discussion. It is up to the guardian to call the terminate function and be the last stand to say “this proposal has truly and negligently failed to follow through on their promises”.

I don’t see the guardians as micromanagers of a proposal. I think the vetting of the proposal should be enough to justify the funding and we just have an escape hatch incase something goes terribly wrong.

No, there are no set timeframes for claiming. A payee can claim whenever they want that is currently available to them. This can be useful for tax purposes and cashflow management.

I think this contract is an alternative to a lump sum payment and not as program for how to manage pro-active grant funding.


On the topic of pro-active grants:

If we want micromanaged pro-active grant funding then I think that is more of a proposal construct than a smart contract. For example, we could use a split contract to fund three separate payment stream contracts that each represent a milestone. That way a guardian could cancel the funds of the second or third payment stream if the first milestone was not met. But that also just adds complexity for how one payment stream basically works right now. Excited to see what degree of preciseness is needed for funding but in general that would require a very qualified and diligent guardian role.

1 Like

A friend ran the contract through a local solidity LLM he’s been building. Based what it spat out, the contract is pretty solid but there are a few optimizations that can made :slight_smile:

Here’s the output:

Here is a detailed review of the `PaymentStream.sol` contract, highlighting potential optimizations and security issues:

### Potential Optimizations

1. **Use of Immutable Variables**:
   - **Current Implementation**:
     ```solidity
     uint256 public immutable duration;
     address public immutable paymentToken;
     uint256 public immutable paymentAmount;
     uint256 public immutable startTime;
     uint256 public immutable endTime;
     address public immutable termReceiver;
     ```
   - **Optimization**:
     These immutable variables are already optimized as they save gas by being set only once at the time of contract deployment. No further optimization needed for these variables.

2. **Reduce Redundant Storage Reads**:
   - **Current Implementation**:
     ```solidity
     uint256 claimable = getClaimableAmount();
     ```
   - **Optimization**:
     Cache values of frequently read storage variables to reduce gas costs:
     ```solidity
     uint256 _claimedAmount = claimedAmount;
     uint256 claimable = (paymentAmount * (block.timestamp - startTime) / duration) - _claimedAmount;
     ```

3. **Emit Event Outside Conditional Blocks**:
   - **Current Implementation**:
     ```solidity
     if (termConfirmations == 1) {
         ...
         emit Terminated(remainingTokens);
     } else {
         ...
         emit TerminationInitiated(msg.sender);
     }
     ```
   - **Optimization**:
     Consider moving the event emission to outside conditional blocks where applicable to reduce code duplication.

4. **Inline SafeMath for Solidity 0.8+**:
   - **Current Implementation**:
     Uses SafeERC20 for ERC20 interactions.
   - **Optimization**:
     Solidity 0.8+ has built-in overflow checks, so using inline operations can sometimes save gas, but using SafeERC20 for security is still recommended.

### Security Issues

1. **Reentrancy Vulnerability in `claim` Function**:
   - **Current Implementation**:
     ```solidity
     uint256 claimable = getClaimableAmount();
     claimedAmount += claimable;
     IERC20(paymentToken).safeTransfer(payee, claimable);
     emit Claimed(claimable, paymentAmount - claimedAmount);
     ```
   - **Security Issue**:
     There is a potential reentrancy attack if the payee is a smart contract.
   - **Mitigation**:
     Use the Checks-Effects-Interactions pattern:
     ```solidity
     function claim() external {
         require(msg.sender == payee, "Not authorized");
         require(claimedAmount < paymentAmount, "All tokens have been claimed");
         require(!isTerminated, "Stream terminated");

         uint256 claimable = getClaimableAmount();
         claimedAmount += claimable;

         emit Claimed(claimable, paymentAmount - claimedAmount);
         
         IERC20(paymentToken).safeTransfer(payee, claimable);
     }
     ```

2. **Authorization Checks in `terminate` Function**:
   - **Current Implementation**:
     ```solidity
     require(termSigners[msg.sender].isSigner, "Not authorized");
     require(!termSigners[msg.sender].hasConfirmed, "Already confirmed termination");
     require(claimedAmount < paymentAmount, "All tokens have been claimed");
     require(!isTerminated, "Stream terminated");
     ```
   - **Security Issue**:
     Multiple require checks can be optimized and grouped logically to ensure clarity and avoid potential bypasses.

3. **Handling of `TermSigner` Initialization**:
   - **Current Implementation**:
     ```solidity
     termSigners[_termSigners[0]] = TermSigner(true, false);
     termSigners[_termSigners[1]] = TermSigner(true, false);
     termSigners[_payee] = TermSigner(true, false);
     ```
   - **Security Issue**:
     Ensure that the `_termSigners` array is properly validated for uniqueness and non-zero addresses to prevent unauthorized terminations.

4. **Gas Limit Considerations for Looping Over Term Signers**:
   - **Current Implementation**:
     This contract does not currently have a looping mechanism over term signers, but consider potential gas limit issues if additional logic is added in the future.

### Summary of Recommendations

- **Optimizations**:
  1. Use caching for frequently accessed storage variables.
  2. Consider emitting events outside conditional blocks where applicable.

- **Security Enhancements**:
  1. Apply the Checks-Effects-Interactions pattern in the `claim` function.
  2. Ensure thorough validation and unique initialization of term signers.
  3. Review and ensure logical grouping of authorization checks.

By addressing these potential optimizations and security issues, the contract can be made more efficient and secure.
1 Like

Interesting concept!

This was the reply from the dev who built the contract:

“Yeah nothing is of substance, and certain things are downright wrong. Only thing is term signer validation, but that costs extra gas that isn’t worth it imo, just both parties have to validate the legitimacy of the stream. LLM won’t be usable for audits in something as nuanced as a smart contract for a long time :wink:”

1 Like


Anyway :grin:

@Titan-Node this is great, so when we will submit our proposal (link here) can we use the Payment Stream Contract?

I believe we will be the first one to use.

The Real Time SPE is actively using it for their treasury funding.

https://explorer.livepeer.org/treasury/21070420873731043589203073883046923976266040019943070396853134324885100838820

But yes we can set up a call to go through it and deploy a test contract to make sure it works with your multisig.

1 Like

That would be great, I’m going to send you a DM in Discord then so we can perform a test together :pray:

@Titan-Node, thanks to your instructions, the process for deploying the contract is quite clear to us. However, we’re unsure about which guardians’ addresses need to be specified.

Could you clarify who is responsible for determining the Guardians?

Thanks for your assistance as usual :pray:

I have an idea for a future V2 stream contract:

  • It would be nice if we could configure an upfront deposit / down payment. IE for teams that have already done a few months of work or have some initial costs they need covered.

  • A completion bonus: I can imagine projects that could run for an indefinite amount of time, where the bulk of the payout happens at the end when certain success criteria are met (at the discretion of the guardians). Maybe better as a separate contract

Skimming through the source code, the first option should be simple to add with 1 additional parameter. (I could try to create a PR for it) WDYT?

Yeah I love the iteration on this type of contract.

Some initial thoughts

  1. You can use a split contract that could allocate an up front payment and send the rest to the stream contract. That wouldn’t need any modifications to the current contract. But if we did a rewrite and audit, a lump sum would be a nice built in feature!

  2. Yeah some sort of payment tranche at the end would also be cool. I know superfluid does this but I can’t seem to find any current OZ contract that does it. Again, would be a sweet V2 feature!

I know the Livepeer ecosystem team is trying to communicate with Superfluid to use their product for our treasury. It might be worth continue trying to integrate with a provider for these streaming contracts. I just wanted to get something out there quickly so we have the option.
But feel free to make adjustments and give it a go!
I’m sure an audit on V2 would be relatively cheap.

1 Like