Created
March 12, 2022 00:26
-
-
Save z0r0z/d970fa0ba5efc6c186d79b1f715d6b22 to your computer and use it in GitHub Desktop.
A simple token streaming manager represented by NFTs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SPDX-License-Identifier: AGPL-3.0-only | |
pragma solidity ^0.8.10; | |
import 'https://github.com/Rari-Capital/solmate/blob/main/src/tokens/ERC20.sol'; | |
import 'https://github.com/Rari-Capital/solmate/blob/main/src/tokens/ERC721.sol'; | |
/// @title lil superfluid nft | |
/// @author Miguel Piedrafita, Ross Campbell | |
/// modified from (https://github.com/m1guelpf/lil-web3/blob/main/src/LilSuperfluid.sol) | |
/// @notice A simple token streaming manager represented by NFTs | |
contract LilSuperfluidNFT is ERC721("LilSuperfluid", "LILS") { | |
/// ERRORS /// | |
/// @notice Thrown trying to withdraw/refuel a function without being part of the stream | |
error Unauthorized(); | |
/// @notice Thrown when attempting to access a non-existant or deleted stream | |
error StreamNotFound(); | |
/// @notice Thrown when trying to withdraw excess funds while the stream hasn't ended | |
error StreamStillActive(); | |
/// EVENTS /// | |
/// @notice Emitted when creating a new steam | |
/// @param stream The newly-created stream | |
event StreamCreated(Stream stream); | |
/// @notice Emitted when increasing the accessible balance of a stream | |
/// @param streamId The ID of the stream receiving the funds | |
/// @param amount The ERC20 token balance that is being added | |
event StreamRefueled(uint256 indexed streamId, uint256 amount); | |
/// @notice Emitted when the receiver withdraws the received funds | |
/// @param streamId The ID of the stream having its funds withdrawn | |
/// @param amount The ERC20 token balance being withdrawn | |
event FundsWithdrawn(uint256 indexed streamId, uint256 amount); | |
/// @notice Emitted when the sender withdraws excess funds | |
/// @param streamId The ID of the stream having its excess funds withdrawn | |
/// @param amount The ERC20 token balance being withdrawn | |
event ExcessWithdrawn(uint256 indexed streamId, uint256 amount); | |
/// @notice Emitted when the configuration of a stream is updated | |
/// @param streamId The ID of the stream that was updated | |
/// @param paymentPerBlock The new payment rate for this stream | |
/// @param timeframe The new interval this stream will be active for | |
event StreamDetailsUpdated(uint256 indexed streamId, uint256 paymentPerBlock, Timeframe timeframe); | |
/// @dev Parameters for streams | |
/// @param sender The address of the creator of the stream | |
/// @param token The ERC20 token that is getting streamed | |
/// @param balance The ERC20 balance locked in the contract for this stream | |
/// @param withdrawnBalance The ERC20 balance the recipient has already withdrawn to their wallet | |
/// @param paymentPerBlock The amount of tokens to stream for each new block | |
/// @param timeframe The starting and ending block numbers for this stream | |
struct Stream { | |
address sender; | |
ERC20 token; | |
uint256 balance; | |
uint256 withdrawnBalance; | |
uint256 paymentPerBlock; | |
Timeframe timeframe; | |
} | |
/// @dev A block interval definition | |
/// @param startBlock The first block where the token stream will be active | |
/// @param stopBlock The last block where the token stream will be active | |
struct Timeframe { | |
uint256 startBlock; | |
uint256 stopBlock; | |
} | |
/// @dev Components of an Ethereum signature | |
struct Signature { | |
uint8 v; | |
bytes32 r; | |
bytes32 s; | |
} | |
/// @notice Used as a counter for the next stream index | |
/// @dev Initialised at 1 because it makes the first transaction slightly cheaper | |
uint256 internal streamId = 1; | |
/// @notice Signature nonce, incremented with each successful execution or state change | |
/// @dev This is used to prevent signature reuse | |
/// @dev Initialised at 1 because it makes the first transaction slightly cheaper | |
uint256 public nonce = 1; | |
/// @dev The EIP-712 domain separator | |
bytes32 public immutable domainSeparator; | |
/// @dev EIP-712 types for a signature that updates stream details | |
bytes32 public constant UPDATE_DETAILS_HASH = | |
keccak256( | |
'UpdateStreamDetails(uint256 streamId,uint256 paymentPerBlock,uint256 startBlock,uint256 stopBlock,uint256 nonce)' | |
); | |
/// @notice An indexed list of streams | |
/// @dev This automatically generates a getter for us! | |
mapping(uint256 => Stream) public getStream; | |
/// @notice Deploy a new LilSuperfluid instance | |
constructor() payable { | |
domainSeparator = keccak256( | |
abi.encode( | |
keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'), | |
keccak256(bytes('LilSuperfluid')), | |
bytes('1'), | |
block.chainid, | |
address(this) | |
) | |
); | |
} | |
/// @notice Create a stream that continously delivers tokens to `recipient` | |
/// @param recipient The address that will receive the streamed tokens | |
/// @param token The ERC20 token that will get streamed | |
/// @param initialBalance How many ERC20 tokens to lock on the contract. Note that only the locked amount is guaranteed to be delivered to `recipient` | |
/// @param timeframe An interval of time, defined in block numbers, during which the stream will be active | |
/// @param paymentPerBlock How many tokens to deliver for each block the stream is active | |
/// @return id The ID of the created stream/NFT | |
/// @dev Remember to call approve(<address of this contract>, <initialBalance or greater>) on the ERC20's contract before calling this function | |
function streamTo( | |
address recipient, | |
ERC20 token, | |
uint256 initialBalance, | |
Timeframe calldata timeframe, | |
uint256 paymentPerBlock | |
) external payable returns (uint256 id) { | |
Stream memory stream = Stream({ | |
token: token, | |
sender: msg.sender, | |
withdrawnBalance: 0, | |
timeframe: timeframe, | |
balance: initialBalance, | |
paymentPerBlock: paymentPerBlock | |
}); | |
emit StreamCreated(stream); | |
id = streamId++; | |
getStream[id] = stream; | |
token.transferFrom(msg.sender, address(this), initialBalance); | |
_mint(recipient, id); | |
} | |
/// @notice Increase the amount of locked tokens for a certain token stream | |
/// @param id The ID for the stream that you are locking the tokens for | |
/// @param amount The amount of tokens to lock | |
/// @dev Remember to call approve(<address of this contract>, <amount or greater>) on the ERC20's contract before calling this function | |
function refuel(uint256 id, uint256 amount) public payable { | |
if (getStream[id].sender != msg.sender) revert Unauthorized(); | |
unchecked { | |
getStream[id].balance += amount; | |
} | |
emit StreamRefueled(id, amount); | |
getStream[id].token.transferFrom(msg.sender, address(this), amount); | |
} | |
/// @notice Receive some of the streamed tokens, only available to the receiver of the stream (holder of NFT) | |
/// @param id The ID for the stream you are withdrawing the tokens from | |
function withdraw(uint256 id) public payable { | |
if (ownerOf[id] != msg.sender) revert Unauthorized(); | |
uint256 balance = balanceOfStream(id, msg.sender); | |
unchecked { | |
getStream[id].withdrawnBalance += balance; | |
} | |
emit FundsWithdrawn(id, balance); | |
getStream[id].token.transfer(msg.sender, balance); | |
} | |
/// @notice Withdraw any excess in the locked balance, only available to the creator of the stream after it's no longer active | |
/// @param id The ID for the stream you are receiving the excess for | |
function refund(uint256 id) public payable { | |
if (getStream[id].sender != msg.sender) revert Unauthorized(); | |
if (getStream[id].timeframe.stopBlock > block.number) revert StreamStillActive(); | |
uint256 balance = balanceOfStream(id, msg.sender); | |
getStream[id].balance -= balance; | |
emit ExcessWithdrawn(id, balance); | |
getStream[id].token.transfer(msg.sender, balance); | |
} | |
/// @dev A function used internally to calculate how many blocks the stream has been active for so far | |
/// @param timeframe The time interval the stream is supposed to be active for | |
/// @param delta The amount of blocks the stream has been active for so far | |
function calculateBlockDelta(Timeframe memory timeframe) internal view returns (uint256 delta) { | |
if (block.number <= timeframe.startBlock) return 0; | |
if (block.number < timeframe.stopBlock) return block.number - timeframe.startBlock; | |
return timeframe.stopBlock - timeframe.startBlock; | |
} | |
/// @notice Check the balance of any of the involved parties on a stream | |
/// @param id The ID of the stream you're looking up | |
/// @param who The address of the party you want to know the balance of | |
/// @return The ERC20 balance of the specified party | |
/// @dev This function will always return 0 for any address not involved in the stream | |
function balanceOfStream(uint256 id, address who) public view returns (uint256) { | |
Stream memory stream = getStream[id]; | |
if (stream.sender == address(0)) revert StreamNotFound(); | |
uint256 blockDelta = calculateBlockDelta(stream.timeframe); | |
uint256 recipientBalance = blockDelta * stream.paymentPerBlock; | |
if (who == ownerOf[id]) return recipientBalance - stream.withdrawnBalance; | |
if (who == stream.sender) return stream.balance - recipientBalance; | |
return 0; | |
} | |
/// @notice Update the rate at which tokens get streamed, or the interval the stream is active for. Requires both parties to authorise the change | |
/// @param id The ID for the stream which is getting its configuration updated | |
/// @param paymentPerBlock The new rate at which tokens will get streamed | |
/// @param timeframe The new interval, defined in blocks, the stream will be active for | |
/// @param sig The signature of the other affected party for this change, certifying they approve of it | |
function updateDetails( | |
uint256 id, | |
uint256 paymentPerBlock, | |
Timeframe calldata timeframe, | |
Signature calldata sig | |
) public payable { | |
Stream memory stream = getStream[id]; | |
if (stream.sender == address(0)) revert StreamNotFound(); | |
bytes32 digest = keccak256( | |
abi.encodePacked( | |
'\x19\x01', | |
domainSeparator, | |
keccak256( | |
abi.encode( | |
UPDATE_DETAILS_HASH, | |
id, | |
paymentPerBlock, | |
timeframe.startBlock, | |
timeframe.stopBlock, | |
++nonce | |
) | |
) | |
) | |
); | |
address sigAddress = ecrecover(digest, sig.v, sig.r, sig.s); | |
address owner = ownerOf[id]; | |
if ( | |
!(stream.sender == msg.sender && owner == sigAddress) && | |
!(stream.sender == sigAddress && owner == msg.sender) | |
) revert Unauthorized(); | |
emit StreamDetailsUpdated(id, paymentPerBlock, timeframe); | |
getStream[id].paymentPerBlock = paymentPerBlock; | |
getStream[id].timeframe = timeframe; | |
} | |
/// METADATA LOGIC /// | |
function tokenURI(uint256) public pure override returns (string memory) { | |
return "PLACEHOLDER"; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment