Providing payment transparency with Indie Protocol's subgraph
How & why we're using The Graph to track payment distributions on Indie Protocol
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 ❤️