3. PartyDAO crowdfund statuses

We create one leaderboard of PartyDAO Crowdfunds and another of contributors within a crowdfund

Goal

Party is a protocol that allows users to start a "crowdfund," a group that collaborates and contributes (Ether) in order to achieve a certain goal, such as buying an NFT.

Anyone can join a crowdfund by contributing Ether to the contract so long as the maximum amount allowed to be contributed to the fund hasn't been reached yet. Once the minimum amount contributed has been reached, the owner of the fund can execute the purpose of that fund, i.e., starting an ERC20 token or buying an NFT.

In the following example, we build one API that gives us a leaderboard of contributors for a specific crowdfund as well as another API that gives us a leaderboard of crowdfunds, ordered by total contributions.

If you want to skip to the end, here's the canvas that already has everything built.


Steps

Setting up the pipeline

Adding components

  1. Start by adding a Data source component. We want our pipeline to run at the tip, so we leave To blank and set From to the Crowdfund Factory's deployment block (17480068).
  2. Add an EVM Lambda component and connect it to the right handle of the data source. For now, leave it as is. We'll come back to it in a bit.
  3. Add a Schema and draw a connection from the lambda to it. Name the schema crowdfund_contributions and add the data we want to persist for our leaderboard APIs:
NameType
global_counteruint256
block_numberuint64
crowdfundaddress
party_namestring
contributoraddress
amountuint256
min_requireduint256
max_requireduint256
total_contributionsuint256
passed_minbool
  1. Add a Persistence component. Connect it to the Schema component.
  2. Finally, add two API components that we will use to query the data in our table. Leave the queries empty for now--we'll come back to them later.

Writing the code

  1. The contracts we want to target are InitialETHCrowdfund contracts. They're all deployed by the same factory and so they all have the same ABI. Therefore, we can use the Import ABI via address functionality in our hooks and provide the address of one InitialETHCrowdfund contract, 0xbda25a6e707be3887ee83e92203a5f6fbcc7775a, to target them all!
  2. Add a post hook on the Contributed log, which is emitted from any contribution to a fund. Our callback will look as follows:
    function handleContribution() public {
        handleContributionContext storage ctx = getHandleContributionContext();
        address cfProxy = simLogAddress();
        uint96 min_req = IInitialETHCrowdfund(cfProxy).minTotalContributions();
        uint96 max_req = IInitialETHCrowdfund(cfProxy).maxTotalContributions();
        uint96 total_contributions = IInitialETHCrowdfund(cfProxy).totalContributions();
        address party = IInitialETHCrowdfund(cfProxy).party();
        string memory party_name = IParty(party).name();
         simEmitToSchema_crowdfund_contributions(
            simGlobalCounter(), // global_counter
            uint64(block.number), // block_number
            cfProxy,// crowdfund
            party_name, // party_name
            ctx.contributor, // contributor
            ctx.amount, // amount
            min_req, // min_required
            max_req, // max_required
            // when you call total_contributions at the time of the log
            // it doesn't include the contribution that triggered the log
            // we add it in here to get an inclusive total
            total_contributions + ctx.amount, // total_contributions
            total_contributions + ctx.amount >= min_req // passed_min
        );
    }

Three notes:

  1. We use the simGlobalCounter to emit a unique identifier for each contribution event. Read more about it here!
  2. Since Crowdfund contracts are proxies deployed by the factory and pointing to the same implementation contract, we use the simLogAddress function to get the address of the proxy itself to get proper values for the methods we call on the crowdfund contract.
  3. When targeting contracts by address, just like we did here, we get an automatically generated interface that is named after the address we've targeted. It can be renamed in the Interfaces tab for convenience to any name you find fit - in our case we've renamed it after the interface of the contracts we're targeting - IInitialETHCrowdfund.

Execution

You can test the execution within the code editor using the range 17487000 to 17487010. You should see one result as there was a single contribution in this range. Then we hit the play button on the execution edge to launch the full tip and backfill jobs.

Setting up the APIs

If we want to do some analysis, we could open the query editor and write many SQL queries against our created data. But let's just go ahead and build some APIs.

  1. Go back to our first API component and use the following query, which will pull the top N contributors for a specific crowdfund.
select contributor,
sum(cast(amount as double)) as contributions 
from @org.crowdfund_contributions
where crowdfund = $crowdfund
group by contributor
order by contributions desc
limit $limit
  1. For the crowdfund and limit parameters, use address and uint32, respectively. Default values are optional, but 0xa3bad5098f9489f536342ca9957bbc808d9d5d96 and 10 make sense. Test the API and activate it.
  2. For the second API, we want to get the top crowdfunds by total_contributions. You can use the same type and default value for the limit parameter as above.
with latest_gc as (
  select crowdfund,
    max(cast(global_counter as double)) as latest_gc
  from @org.crowdfund_contributions
  group by crowdfund
)
select b.crowdfund, b.party_name, b.block_number, b.min_required, b.max_required, b.total_contributions,
  ((1.0 * b.total_contributions) * 100 / (1.0 * b.min_required)) AS pct_of_min,
  ((1.0 * b.total_contributions) * 100 / (1.0 * b.max_required)) AS pct_of_max,
  passed_min
  from @org.crowdfund_contributions b
  inner join latest_gc lgc on b.crowdfund = lgc.crowdfund and b.global_counter = lgc.latest_gc
order by total_contributions desc
limit $limit

Give them a try using the test interactions and the cURL requests! Note that this setup also easily supports time-travel, i.e., you can get the contributor leaderboard or crowdfund leaderboard at any point in time by including where block_number \<= [some_block_number]\ in the queries. You could also make the block number a parameter to support time-traveling in the API request.

Key takeaways

  1. sim's unique Global Counter allows us to order emitted messages, even within the same internal transaction!
  2. sim's Input ABI via address makes it a breeze to target all contracts implementing the same interface.
  3. We can create as many APIs as we want and point them to the same table.

Join our Telegram