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
- 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).
- 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.
- 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:
Name | Type |
---|---|
global_counter | uint256 |
block_number | uint64 |
crowdfund | address |
party_name | string |
contributor | address |
amount | uint256 |
min_required | uint256 |
max_required | uint256 |
total_contributions | uint256 |
passed_min | bool |
- Add a Persistence component. Connect it to the Schema component.
- 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
- 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 theImport ABI via address
functionality in our hooks and provide the address of oneInitialETHCrowdfund
contract,0xbda25a6e707be3887ee83e92203a5f6fbcc7775a
, to target them all! - 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:
- We use the
simGlobalCounter
to emit a unique identifier for each contribution event. Read more about it here! - 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. - 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.
- 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
- For the
crowdfund
andlimit
parameters, useaddress
anduint32
, respectively. Default values are optional, but0xa3bad5098f9489f536342ca9957bbc808d9d5d96
and10
make sense. Test the API and activate it. - 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
- sim's unique Global Counter allows us to order emitted messages, even within the same internal transaction!
- sim's
Input ABI via address
makes it a breeze to target all contracts implementing the same interface. - We can create as many APIs as we want and point them to the same table.
Updated about 1 month ago