The Indie Zone

Share this post

Providing payment transparency with Indie Protocol's subgraph

indies.substack.com

Discover more from The Indie Zone

Deciphering the latest development, design and tech news.
Continue reading
Sign in

Providing payment transparency with Indie Protocol's subgraph

How & why we're using The Graph to track payment distributions on Indie Protocol

Sean Connolly
Aug 24, 2023
2
Share this post

Providing payment transparency with Indie Protocol's subgraph

indies.substack.com
Share

We recently published the Indie Protocol Subgraph to help with IndieDAO’s operational needs as a professional services DAO.

This post offers insight into Indie Protocol, why we saw the need to create a new subgraph to support the protocol and how we implemented the subgraph.

What is Indie Protocol?

Indie Protocol is a ground-breaking solution that matches dream teams to dream projects based on a comprehensive analysis of project goals and team skillsets.

Indies who specialize in development and design services use the protocol to manage client projects from start to finish. The protocol covers everything required for a project including legal agreements, budget proposals, deposits, team formation, weekly deliverables and payouts.

What is a subgraph?

The Graph hosts subgraphs, which are open APIs that organize and serve blockchain data to applications.

Subgraphs are especially helpful in cases where smart contract storage is not structured to support application or analytics use cases. One of our prior posts, Composable protocols powering beautiful purpose-built apps goes into detail on the challenges of building custom blockchain indexing and aggregation solutions but if a subgraph can work for your use case, it is generally a more efficient option.

Indie Protocol’s infrastructure

Indie Protocol is built as a decentralized web application (dApp) that interacts with an Ethereum smart contract that we call The Broker.

Indie Protocol’s smart contract

Before we can explain why we need a subgraph, it’s important to understand a few details about The Broker smart contract, which is really the brains of the protocol.

The Broker’s main purpose is to hold client deposits in custody while indies work on a project. When deliverables are accepted by the client, The Broker distributes payments to indies who worked on the project along with rewards to other participants who helped along the way.

Completing a sprint

In Indie Protocol, each week is a sprint that has its own resources, objectives and costs.

The Broker smart contract has a completeSprint function which is where deposits are distributed as payouts to various project participants.

Note the most important actions here are USDC transfers to various addresses including the payouts to the indies who worked on the project!

Also take note of the DistributePayment event, which is in bold. We’ll discuss that one later.

The code below has been abridged for clarity:

function completeSprint(
        uint256 projectId,
        uint256 sprintId,
        address[] calldata payees,
        uint256[] calldata amounts,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external {
        SprintPayments memory sprintPayments = calculateSprintPayments(payees, amounts);

        for (uint256 i = 0; i < sprintPayments.payeeAmounts.length;) {
            emit DistributePayment(
                payees[i],
                projectId,
                amounts[i],
                sprintPayments.payeeAmounts[i],
                sprintPayments.treasuryAmounts[i],
                sprintPayments.leadAmounts[i],
                sprintPayments.salesAmounts[i],
                sprintPayments.cashVestingAmounts[i]
            );

            if (sprintPayments.payeeAmounts[i] > 0) {
                usdc.transfer(payees[i], sprintPayments.payeeAmounts[i]);
            }

            unchecked {
                i++;
            }
        }

        if (sprintPayments.totalTreasuryAmount > 0) {
            usdc.transfer(feeRecipients[Fees.Treasury], sprintPayments.totalTreasuryAmount);
        }

        if (sprintPayments.totalCashVestingAmount > 0) {
            usdc.transfer(feeRecipients[Fees.CashVesting], sprintPayments.totalCashVestingAmount);
        }

        if (sprintPayments.totalLeadAmount > 0) {
            usdc.transfer(project.leadAddress, sprintPayments.totalLeadAmount);
        }

        if (sprintPayments.totalSalesAmount > 0) {
            usdc.transfer(project.salesAddress, sprintPayments.totalSalesAmount);
        }

        projectBalances[projectId] -= sprintPayments.totalAmount;
        completedSprints[projectId][sprintId] = true;

        emit CompleteProjectSprint(projectId, sprintId, sprintPayments.totalAmount);
    }

While this function does its job well - distributing payments to the parties it needs to - it is difficult to answer a few questions:

  • As an indie, how much did I earn in a given month, quarter, year?

  • As an indie, how much did I earn from sales rewards?

  • How much was paid out in lead rewards last quarter?

  • How much did IndieDAO’s treasury earn through the protocol last month, quarter, year?

A series of USDC transfers alone does not make it easy to answer these questions. Storing all of this data in the smart contract for analytics purposes would also increase gas costs significantly.

🤔 So what do we do?

Why use a subgraph?

As stated earlier, subgraphs are especially helpful in cases where smart contract storage is not structured to support application or analytics use cases. The questions above are analytics questions - ones we want to answer retroactively based on prior protocol activity.

Subgraphs are specifically good for aggregating events into queryable data models and that is exactly what we need.

Building Indie Protocol’s Subgraph

There are a few steps involved with setting up a subgraph but once it is setup, it stays up-to-date on its own and all data is available through a robust GraphQL API.

Logging data with events

From the Solidity docs:

Solidity events give an abstraction on top of the EVM’s logging functionality.

Recall earlier that we highlighted the DistributePayment event, which is structured like so:

event DistributePayment(
  // Address of the indie
  address indexed payee,

  // Associated project
  uint256 indexed projectId,

  // Total amount paid
  uint256 totalAmount,

  // Amount paid to the indie
  uint256 payeeAmount,

  // Amount paid to the IndieDAO treasury
  uint256 treasuryAmount,

  // Amount paid to the lead
  uint256 leadAmount,

  // Amount paid to the sales referrer
  uint256 salesAmount,

  // Amount allocated for the indie's cash vesting
  uint256 cashVestingAmount
);

Reward percentages like lead, sales, treasury, etc. are subject to change over time but a example data might look like this (amounts are 6-decimal USDC):

{
  "payee": "0x123...",
  "projectId": 368070458891829313,
  "totalAmount": 1000000000 // $1,000
  "payeeAmount": 550000000 // $550
  "treasuryAmount": 200000000 // $200 (20%)
  "leadAmount": 100000000 // $100 (10%)
  "salesAmount": 100000000 // $100 (10%)
  "cashVestingAmount": 50000000 // $50 (5%)
}

Semantically, a DistributePayment is saying:

For project 36807… Indie with address 0x123 was paid $1,000 and contributed $200 to the treasury, $100 to the project lead, $100 to the project referrer, $50 to their cash vesting and kept $550 as take home.

Reading events into a subgraph

Now that we know events are being logged for each payment, how do we read these into a subgraph to make it easier to query this data?

All of this code is public and be found here:

https://github.com/indiedao/indie-protocol-subgraph

With any subgraph, you typically need to specify a contract to watch and the events or function calls you want to monitor. Below is an abridged snippet of the subgraph.yaml file.

specVersion: 0.0.5
schema:
  file: ./schema.graphql
dataSources:
  - kind: ethereum
    name: IndieBrokerV1
    network: mainnet
    source:
      address: "0x72A78d4Da171fbF382aF0C3Dae94949e1459852f"
      abi: IndieBrokerV1
      startBlock: 16533931
    mapping:
      kind: ethereum/events
      apiVersion: 0.0.7
      language: wasm/assemblyscript
      entities:
        - DistributePayment
      abis:
        - name: IndieBrokerV1
          file: ./abis/IndieBrokerV1.json
      eventHandlers:
        - event: DistributePayment(indexed address,indexed uint256,uint256,uint256,uint256,uint256,uint256,uint256)
          handler: handleDistributePayment

The subgraph is configured to watch The Broker at 0x72A7… and to monitor the DistributePayment event and call a handleDistributePayment function for each event.

When deployed to The Graph, their infrastructure will read block transactions, block by block, and call into our specified functions each time it sees a transaction related to The Broker smart contract.

Now let’s look at the handleDistributePayment function (abridged again for clarity):

export function handleDistributePayment(event: DistributePaymentEvent): void {
  _updatePaymentSummary("allTime", entity)

  const quarter = _findOrCreateQuarterFromTimestamp(event.block.timestamp)
  const paymentSummary = _updatePaymentSummary(quarter.id, entity)
  quarter.paymentSummary = paymentSummary.id
  quarter.save()

  entity.save()
}

There are several additional functions here but from top to bottom you can see each event:

  • Updates an “allTime” payment summary

  • Updates a quarterly payment summary

A PaymentSummary is an entity in the subgraph. You can think of it like a database table where each new entry is a row with a unique identifier.

Entities are defined in the schema.graphql file like so:

type PaymentSummary @entity {
  id: ID!
  totalAmountSum: BigInt! # uint256
  payeeAmountSum: BigInt! # uint256
  treasuryAmountSum: BigInt! # uint256
  leadAmountSum: BigInt! # uint256
  salesAmountSum: BigInt! # uint256
  cashVestingAmountSum: BigInt! # uint256
}

In our case, we’ll have an entity identified as the “allTime” payment summary that increments these values for every new DistributePayment event.

We will also have one entity per quarter, identified through a year_quarter convention, e.g. 2023 Q1 would be “2023_1”.

The function below increments the values of a payment summary based on its identifier:

function _updatePaymentSummary(id: string, payment: DistributePayment): PaymentSummary {
  const summary = _findOrCreatePaymentSummary(id)
  summary.totalAmountSum = summary.totalAmountSum.plus(
    payment.totalAmount
  )
  summary.payeeAmountSum = summary.payeeAmountSum.plus(
    payment.payeeAmount
  )
  summary.treasuryAmountSum = summary.treasuryAmountSum.plus(
    payment.treasuryAmount
  )
  summary.leadAmountSum = summary.leadAmountSum.plus(
    payment.leadAmount
  )
  summary.salesAmountSum = summary.salesAmountSum.plus(
    payment.salesAmount
  )
  summary.cashVestingAmountSum = summary.cashVestingAmountSum.plus(
    payment.cashVestingAmount
  )
  summary.save()

  return summary
}

Dealing with time in a subgraph

Time is a weird thing in blockchain applications and it’s especially weird when working with subgraphs.

Notice how the DistributePayment event itself does not have any concept of time on it. In order to determine when the event occurred, we need to inspect the timestamp of the event’s block. Remember events are part of transactions, which are part of blocks.

This line is where we read the timestamp and use it to determine in which quarter the event occurred.

const quarter = _findOrCreateQuarterFromTimestamp(event.block.timestamp)

Now let’s look at the _findOrCreateQuarterFromTimestamp, which does some date math to convert the timestamp to a quarter. EVM timestamps are stored as seconds since the epoch so we first convert the timestamp to milliseconds to create a JavaScript Date and then extract UTC year and quarter from the Date. Keep in mind, The Graph works with AssemblyScript so your favorite date/time libraries like date-fns are unfortunately not available.

function _findOrCreateQuarterFromTimestamp(timestamp: BigInt): Quarter {
  const timestampAsNumber = timestamp.toI64()
  const timestampInMilliseconds = timestampAsNumber * 1000
  const timestampAsDate = new Date(timestampInMilliseconds)
  const year = timestampAsDate.getUTCFullYear()
  const month = timestampAsDate.getUTCMonth()
  const quarter = _getQuarterFromMonth(month)

  return _findOrCreateQuarter(year, quarter)
}

Querying the subgraph

Once a subgraph is deployed, it’s easy to query data since it is all available as a public GraphQL API.

The easiest way to query Indie Protocol data is through the hosted playground, which can be found here:

https://thegraph.com/hosted-service/subgraph/indiedao/indie-protocol-subgraph

All of the sample results below were queried in June of 2023.

Querying all time data

Query:

{
  paymentSummary(id: "allTime") {
    totalAmountSum
    payeeAmountSum
    leadAmountSum
    salesAmountSum
    treasuryAmountSum
    cashVestingAmountSum
  }
}

Result:

{
  "data": {
    "paymentSummary": {
      "totalAmountSum": "385680000000", // $385,680
      "payeeAmountSum": "212124000000", // $212,124
      "leadAmountSum": "38568000000", // $38,568
      "salesAmountSum": "38568000000", // $38,568
      "treasuryAmountSum": "77136000000", // $77,136
      "cashVestingAmountSum": "19284000000" // $19,284
    }
  }
}

Querying quarterly data

Query:

{
  quarters(where: {year: 2023}) {
    id
    quarter
    paymentSummary {
      totalAmountSum
      payeeAmountSum
      treasuryAmountSum
      salesAmountSum
      leadAmountSum
      cashVestingAmountSum
    }
  }
}

Result:

{
  "data": {
    "quarters": [
      {
        "id": "2023_1",
        "quarter": 1,
        "paymentSummary": {
          "totalAmountSum": "93560000000", // $93,560
          "payeeAmountSum": "51458000000", // $51,458
          "treasuryAmountSum": "18712000000", // $18,712
          "salesAmountSum": "9356000000", // $9,356
          "leadAmountSum": "9356000000", // $9,356
          "cashVestingAmountSum": "4678000000" // $4,678
        }
      },
      {
        "id": "2023_2",
        "quarter": 2,
        "paymentSummary": {
          "totalAmountSum": "292120000000", // $292,120
          "payeeAmountSum": "160666000000", // $160,666
          "treasuryAmountSum": "58424000000", // $58,424
          "salesAmountSum": "29212000000", // $29,212
          "leadAmountSum": "29212000000", // $29,212
          "cashVestingAmountSum": "14606000000" // $14,606
        }
      }
    ]
  }
}

Wrapping up

Accessing and understanding these metrics is an important part of our community at IndieDAO and the Indie Protocol subgraph is a critical first step in that direction. It helps us see how we are doing as a cooperative community and will also help indies see their success over time. But we aren't done yet! We have many improvements in the works, including fun ways to visualize the data in a more human-readable and consumable format.

We’re here to help! Reach out to Indie if you want to collaborate on a project ❤️

2
Share this post

Providing payment transparency with Indie Protocol's subgraph

indies.substack.com
Share
Comments
Top
New
Community

No posts

Ready for more?

© 2023 indiedao.eth
Privacy ∙ Terms ∙ Collection notice
Start WritingGet the app
Substack is the home for great writing