Git Product home page Git Product logo

buoyant's Introduction

Buoyant

"Subs not dubs."

Demo (Local)

To demo our app's functionality, we've created a basic control panel/dashboard where you can test out creating and renewing subscriptions. To run it locally, run the following commands:

// clone the repository
git clone https://github.com/IlliniBlockchain/buoyant.git

// go into the demo app's directory
cd buoyant/website

// install dependencies
npm install

// run locally
npm run start

Project Overview

Description

A base protocol implementing recurring payments.

Motivation

Recurring payments are a very common and helpful payments model. They're used for time-bound access to content/services and for consistent streams of revenue. So far we haven't seen anything that really executes on this basic payments primitive, and also at the time of creating this there are <2 weeks left in Solana's Riptide Hackathon and we wanna build.

Primary Features

  • Allow a user to create a new subscription

  • Allow a payer to deposit into a vault

  • Expose a public function that either

    1. Transfers money from vault to payee and renews the subscription if a certain balance is met
    2. Marks the subscription inactive

    and compensates the caller.

  • Make subscription activity/inactivity tied to a token

Design

TL;DR create a subscription with metadata and get a new NFT for each period the subscription is active. Anyone/cranks will be run to renew/deactivate subscriptions and they'll get a fee paid to them to incentivize.

When someone opens up a new subscription, they'll specify an amount of money to be paid and a duration of how long the subscription is active for. This will then initialize a new account with subscription metadata as well as a new token vault account for the payer to deposit to.

On initialization and renewal of a subscription, an new token mint is created and a token is minted to the payer (and a new associated token account for this particular mint is created as needed). A user's subscription is said to be "active" if the user owns a token from the particular mint. The latest mint's public key will be stored and updated in the subscription's metadata so that products built on top of this protocol have easy access to the necessary data needed to check the active/inactive status of a subscription.

With the issuing of many individual tokens for each subscription, products building on this protocol cannot just check the possession of a specific token as each token is from a different mint. However, in this case, they can just check that the metadata of the subscription--most importantly the amount and duration--match their requirements.

When a subscription is renewed, a new mint will be created. If the deposit vault has enough tokens to renew, the amount will be transferred to the payee and a token from the new mint will be minted to the payer.

The renewal instruction will be available for anyone to call. A certain percentage of the amount being transferred to the payee will be sent to the caller of the renewal instruction to incentivize a decentralized participation to enforce the renewal/expiry mechanic of the protocol. In the beginning, a centralized crank will be run to ensure the timely functioning of renewals/expirations. Overtime as usage grows, ideally fee incentives should naturally decentralize this mechanic.

Token Functionality

Because the subscription is represented with a token, it turns them into assets that are tradable on open markets. This not only allows for the simple transfer of a subscription if someone wants to gift it to a friend, but it also allows the ability to sell a subscription for a different duration than the original services' configuration, e.g. if you paid for a month-long subscription and after 20 days you no longer care for it, you could sell it to someone who might be looking for a 10-day trial for cheaper.

Accounts

Flowchart coming soon.

Roadmap

  • Basic instruction implementations
    • Account flow chart
  • Create email, twitter, and gitbook
  • Integrate Metaplex token metadata standard
  • Create a basic API for other projects to build on
    • Javascript/Typescript types and instruction wrappers
    • Rust instruction wrappers
    • Anchor accounts and contexts
  • Crank/public renewal assistance

buoyant's People

Contributors

alecchendev avatar jdubpark avatar kevinz420 avatar vsiva308 avatar zfaizal2 avatar

Stargazers

 avatar  avatar  avatar

buoyant's Issues

๐ŸŽฅ Create Video Submission

  • Maybe do a meme-y intro of us trying to find a project and just picking some random one off the riptide ideas list lol
  • Maybe make some basic slides just for transitions and stuff
  • Use website and/or docs to flow through an overview of what it is and how it works
  • Use frontend demo app to demo functionality/basic product built on the protocol

๐ŸŽฎ Demo Frontend

  • For Riptide you can submit slides or a video - to show functionality I think a small frontend dashboard/control panel would be helpful to show it, and we can run a crank from a node.js script
  • Film with loom

Update README.md

Now that gitbook docs are launched, we can change readme to more of a basic overview and contribution guidelines

  • still keep basic description
  • links to website, twitter, docs
  • how to build and deploy program
  • overview of scripts and how to run
  • running our website - look in head tag for a helpful comment, also its literally an html file lmao
  • contributing - basically our engineering flow: assign yourself an issue, create new branch, dummy commit, draft PR, make changes, change PR to be ready for review, request review, iterate, merge
  • Also add a part on why we went with raw solana and not anchor - mainly we wanted to build and hadn't learned anchor yet but we had alr gotten started with raw SOL for fundamentals/intuition

๐Ÿ—ป Store bumps and other seeds in PDAs

becoming a sort of dev ux standard + saves on compute inside program instructions, which is something we could feasibly care about - rn a full renew takes up ~15k/20k compute units

main tasks here:

  • for all existing PDAs (besides token/mint accounts)
    • add a bump field in the state.rs
    • if the program initializes the PDA, make sure to initialize it with enough space to hold the bump
    • whenever that PDA is passed into an instruction and validated, use create_program_address and include the bump as a seed
      • change the current check_pda helper to maybe take in another with_bump: bool parameter and use create_program_address or find_program_address based on that
      • OR add a check_pda_with_bump function

note:

  • for Subscription metadata, please add the bump at the end of the struct because my initialize.js script deserializes some stuff with kinda hardcoded values so adding it at the beginning would break that, but at the end its all good.
  • also for subscription metadata, store the count seed

also:

  • if whoever's implementing this has the time, throw in changes to check_initialized_ata to 1) be renamed to check_ata_initialized and run check_ata within it so you don't have to run both

๐Ÿ›‘ Move mint initialization

  • Realized that because we can just mark the active flag and set a new next_renewal_time, we don't really need to create a new mint to "revoke" the subscription
  • Also in this case, you should make sure to initialize the mint/token in initialize
  • See #56 for related info

๐Ÿ“Ž Subscription Registry

Would be nice to have this before launch/video demo

Basic outline

  • two accounts - 1 holding a long ass list, 1 holding count
  • data is just straight bytes denoting parameters (32 for payee key, 8 for amount, 8 for duration = 48 bytes total per entry)
  • start with just one account (max size = 10MB = 10,000,000B = ~200,000 48B entries), can upgrade later if needed
  • all the function does is
    • validates the pda
    • takes current list, withdraws rent from account to signer, allocates new account with space for a new entry (payer is signer), appends new entry to list and populates new account data

PDA seed schemes:

let registry_seeds = &[
    b"subscription_registry",
    &count.to_le_bytes(),
];
let counter_seeds = &[
    b"registry_counter",
];

Question

  • Should subscriptions just be automatically be added to registry upon the first creation of one? (default: no)
    • would mean that literally every subscription is on the registry but also could clog registry with dummy/test subscriptions that don't actually need to be stored
    • also ig if products wanted to limit renewals to their own cranks opting out of the registry is helpful ig... but if ppl paying then the info is out there...

Checking

  • Check if the subscription is actually initialized when they go to add it by taking in the count as an input
  • This way someone cannot just bullshit a list

To Do

  • Constant time lookup (prevent duplicating a subscription using hash map)
  • Also possible solution to previous question, could allow user the option to add a subscription to registry with an input bool

"Tradable Subscriptions" Messing Up Spacing

Didn't realize this till it was already up - 2 lines for "tradable subscriptions" makes the cards uneven - fix by making font smaller or smth..? Maybe even change back to "liquid subscriptions"

image

๐Ÿงฎ Examples

Basic examples showcasing the usage of our protocol + APIs. See linked issues.

Make a new folder called examples and put them in here.

๐Ÿ”’ Zero out account data on expiry

  • When withdrawing rent upon expiry, we expect the runtime to delete the account, however this is only valid between transactions. If someone where to insert an instruction after this that uses the dormant account data in a malicious way, this could be bad.

Action:
In the renew instruction code, when it expires a subscription (sets lamports to 0), also zero out the data.

๐Ÿ’… Website touch-ups

  • Noted in #16 - need to add links to docs and frontend demo before launch
  • Also change "Liquid Subscriptions" to "Tradable Subscriptions"
  • Make the background color get darker faster as you scroll
  • Also add description head tag for basic SEO
  • Change tab titile from "Buoyant Protocol" to "Buoyant"

๐ŸŸฆ Typescript API: Setup

  • Atomic task for #9
  • Setting up the this API is composed of a few main things
    • Configuring typescript
    • Setting up to publish an npm package - how do you structure things, export things, etc. such that someone can use everything they need to easily when they install note modules
    • How to actually publish to npm package registry

Possibly helpful resources:
General setup: https://www.tsmean.com/articles/how-to-write-a-typescript-library/
Unit testing: https://www.tsmean.com/articles/how-to-write-a-typescript-library/unit-testing/
Example in practice: https://github.com/solana-labs/solana-program-library/tree/master/token/js

๐Ÿ Re-initialize Expired Subscriptions

When subscriptions expire, the rent from the deposit vault and metadata accounts are withdrawn, so if someone wants to use that subscription again, they need to reinitialize it. To differentiate subscriptions of the same type (i.e. payee, amount, duration) we store a count in a separate account and increment it every time we create a new subscription. If we reinitialize a subscription, we don't want to increment.

2 things to do

  1. Have count be passed into the instruction as an argument
  2. If count != latest count, don't increment

โš™๏ธ Rust Program API: Withdraw Instruction Wrapper

Status: awaiting #15 before we'll be able to test this fully

  • Atomic task for #10 (see for additional details)
  • In instruction.rs add a public wrapper function that makes it easier for other programs building on buoyant to create an instruction for Withdraw when performing a cross program invocation

โš™๏ธ Rust Program API: Registry Instruction Wrapper

Status: awaiting #26 before will be able to test/implement fully

  • Atomic task for #10 (see for additional details)
  • In instruction.rs add a public wrapper function that makes it easier for other programs building on buoyant to create an instruction for [whatever the registry instruction is going to be called] when performing a cross program invocation

๐ŸŒ‘ Make expire only close token account

  • Seems unnecessary/bad devx to get rid of the metadata upon an expire
  • Change this so that crank is only compensated by closing deposit account (if not enough funds left)
  • Note: upon reinitialization, a new mint/token will be created, and if there are funds left in deposit vault, they'll be transferred to current owner - will need to change tests/APIs to make this seemless

Renew Instruction

Need to add two more accounts to the list - the current mint token account of the payer and the payer themselves - you want to mint the next token to the current owner of the subscription, which is denoted by the possession of a token in a token account matching the mint specified in the subscription metadata

In the case that it's the first time a subscription is being paid, the client can pass in either an arbitrary account as the mint token account, or we can define the instruction to make this account optionally. The payer is necessary for all renewals for the creation of the new mint token account.

Also buffer for renewing before expiry??

Is Close a necessary instruction?

Given subscriptions are transferrable, should the original creator of a subscription be allowed to close/withdraw rent from the metadata account? (even if it's expired?) Or really the current owner of the subscription should be allowed to close/withdraw rent

Don't know if it's really a problem but this would have PDA fragmentation, as the count of subscriptions is just incremented - maybe this could be good, if people close a subscription, they could be reused? would add a little complexity to client side (would need to loop through all PDAs to check if they're closed to find one) and would need to add a check program side to accomodate for previously initialized instructions

Rust Program API

  • Similar to #9 but for on-chain programs (and in Rust)
  • Should make it easy to use CPIs for other on-chain programs to interface with our protocol
  • Will involve publishing our program + api as a crate

Note for implementing: SPL libraries are a great resource as always! The token program does exactly what we're looking to do - link to their instructions + wrappers: https://docs.rs/spl-token/latest/src/spl_token/instruction.rs.html#1-1522

Withdraw Instruction

Need to add an account to this instruction: the payer's token account for the token denoting ownership of the subscription. This is to ensure only the rightful owner of the subscription is able to withdraw funds from the deposit vault.

Implementing this check: 1. check that this token account's mint matches the mint specified in the subscription metadata 2. check the amount in the token account is > 0 3. bam

๐Ÿ˜ Crank script

Example script for cranks to use. Written in Rust.

Allow two options: 1) run crank for a certain subscription type 2) run crank over registry.

Ideas for implementation

Brute force: Basically just loops through registry, for each subscription type, it finds the total count, loops through each address, sends renew instruction
Time sensitive: Loops through everything and retrieves next renew times, sorts the next renew times earliest to latest, sleeps until that time, then sends tx.

Subscription Metadata PDA Seed Scheme

Initially when creating a test script for the initialize instruction I had set the PDA seed scheme to be ["subscription_metadata", user, payee, amount, duration], but now I'm thinking otherwise:

  • reason to include user is to have seed ensure account was created by this user, mainly for closing later, but doesn't seem necessary
  • with just "...", payee, amount, duration, we need a way to differentiate many of the same kind of subscription - atm you'd only be able to initialize 1 subscription with these params, and then you'd run into an "already initialized" error

Ideas for new scheme:

  • Just these seeds + a counter number at the end
    • having these params as seeds is wicked - products will easy lookups when running cranks as subscriptions of the same kind will all have these seeds
    • downside is to have this number, we need to make another account to store this (and would check + increment everytime initialize is called) - a lil clunky ux on the dev side (clients initializing would have to check the counter account to get the latest number whenever initializing) but idrk another way, and it's not thaat much of a hassle, also could be abrastracted away if we provide our own wrappers/API

Website

We really launching a product ๐Ÿ’ช

Typescript API

  • Expose an interface to make it easier for people to build on top of this
  • This would include...
    • Classes for accounts and deserialization methods
    • Instruction wrappers
    • Other helper functions
  • Would involve publishing an npm package

Basically this file but on steroids - also some of the following are already implemented in the previously mentioned file, just need to separate, add types, comments/documentation, and figure out file structure for npm package

Outline

  • Constants: BUOYANT_PROGRAM_ID, FEE, FEE_DECIMALS etc.
  • Subscription class
    • Metadata fields
    • Deserialization through constructor - given account data bytes (Uint8Array), or normal necessary fields
  • Instruction wrappers
    • Initialize
    • Withdraw
    • Renew
    • [Registry instr]
  • Other helper functions
    • getSubscriptionPDA(payee, amount, duration, count)
    • helpers to retrieve certain accounts needed for instructions when you only have certain data

๐Ÿงฎ Examples: Restricted Content

  • Atomic task for #50
  • Create a minimal full-stack web app that interfaces with our protocol
  • Frontend - button to create subscription of a required type, button to send request to backend to view some data
  • Backend - stores some file with an arbitrary message, exposes a GET endpoint that will only return the file's contents if the sender has an active subscription of the given type

๐Ÿงฎ Examples: On-Chain Permissions

  • Atomic task for #50
  • Create a minimal on-chain program + dapp that interfaces with our program
  • Frontend - button to create subscription of required type, button to mint an NFT
  • Program - single instruction, checks a provided subscription metadata, if it's the required type and it's active, then mint an NFT to the user (and create necessary associated token account if needed)

๐Ÿšง Missing expiry incentive

Right now people are incentivized to enforce renewals because when funds are transferred, a proportion of that fund is paid to the caller. This is not the case for expiry!

On expiry, it's possible that there are no funds left in the deposit vault, so the caller can only be paid by withdrawing rent from either the deposit vault account or the metadata account, effectively closing the subscription. See #13

Actions:

  • Problem: don't want to withdraw rent/close account if there are tokens left, need some way to get paid. Solution...
    • In Renew, if number of tokens in deposit vault is 0, withdraw rent from subscription and call CloseAccount on token program to close deposit vault
    • In Renew, if number of tokens in deposit vault is >0, take same amount of tokens that you would've taken upon a renewal - if there are less than that, just take whatever's left.
  • Problem: expired accounts that had their rent taken from them need/need not be reinitialized?
    • Option 1: don't let people reinitialize them, whenever initialized is called it only initializes new account (default)
    • Option 2: reinitialize old account
      • reinitialize, need user to pass in count seed
      • Don't increment counter

Also:

  • Add in check_initialized_ata for the payee and caller token accounts if they are already initialized

๐Ÿฅ˜ V2 Migration

High Level

Current design: 1 of 1 NFTs representing ownership of every single subscription, new mint/NFT for every renew, raw on-chain subscription metadata
New design: 1 mint per subscription type, no new mint per renew, metaplex NFT metadata as subscription metadata

Other stuff - write it in anchor, include bumps in account data

Order of Execution

  1. no new mint per renew
  2. metaplex NFT metadata as subscription metadata
  3. 1 mint per subscription type

๐ŸŒˆ Add withdraw restriction

With the new initialize and renew (#66), it is required that a minimum of the renewal fee is present in the deposit vault such that callers are still incentivized to expire accounts (and you don't have to withdraw rent to compensate the caller).

Need to modify withdraw so that the user cannot withdraw past this minimum amount. Should a user want to withdraw the full amount, they can call the Close instruction.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.