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
Set up the Lambda
-
Add an EVM Lambda to the canvas. Under Hook type select ABI and the input via address:
0xbda25a6e707be3887ee83e92203a5f6fbcc7775a
. This is an InitialETHCrowdfund contract. Since we're using the ABI hook, we're hooking on all contracts that implement this interface. Select theContributed
log hook and add it as a Post hook with a callback namedhandleContribution
. Toggle on theGenerate schema
feature as it'll give us a good template. -
A schema will be automatically generated. Click the menu next to the schema and select
Edit
. Add these additional fields to the auto-generated schema: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 -
Open the Interfaces sidebar and input the address for an InitialETHCrowdfund contract (
0xbda25a6e707be3887ee83e92203a5f6fbcc7775a
) and search. Once you've done this, you'll see the added interface both in the sidebar and the Interface code tab within the code editor. Rename the interface by replacingI0xbda25a6e707be3887ee83e92203a5f6fbcc7775a
withIInitialETHCrowdfund
. -
When you're done editing the schema, hit
Save
. Now we need to write our Lambda code:
contract UserProbe is BaseDeclarativeProbe {
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(SchemaCrowdfund_contributionsColumns({
global_counter: simGlobalCounter(),
block_number: uint64(block.number),
crowdfund: cfProxy,
party_name: party_name,
contributor: ctx.contributor,
amount: ctx.amount,
min_required: min_req,
max_required: max_req,
total_contributions: total_contributions + ctx.amount,
passed_min: total_contributions + ctx.amount >= min_req
}));
}
}
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
. -
Test your code with the Test button at the top-right of the IDE. You can use the range
17487000
to17487010
. You should see one result as there was a single contribution in this range.
Build the pipeline
-
Close the IDE. Click on the left handle of the Lambda component to add a
Data source
. Set From to 17480068, as this is when the Crowdfund Factory was deployed on Ethereum. Leave the To block blank as we'll let our code catch up to the tip and then continue to execute. -
Click on the right handle of the Schema component to add a
Persistence
. Name the persistencecrowdfund_contributions
and set it. -
Hit the play button on the execution edge. The code will start executing at the tip and backfilling from the From block. You can see its status by mousing over the
(?)
.
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.
- Add an API component to the canvas. It doesn't have to be connected to any other components. For the first API, let's do a simple one that shows 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