⛓️Technical Details
Overall Flow
Campaign Managers create an on-chain campaign, and deposit
ETH
orERC20s
towards thetotalBudget
of the campaign. This is currently accessible through Spindl's Web App (where a trusted worker can do certain administrative actions on behalf of the campaign manager)Relevant Recipients (or Publishers) are added to each campaign (using
addRecipients
). They generate traffic to fulfill the campaign's criteria. For example a recipient can be an influencer that drives traffic for certain purchases such as an NFTRecipients can also be implicitly created via
updateBalances
which is explained in detail further downThe Spindl server updates the latest balances for the given campaign and its recipients. Spindl will do this on a recurring, periodic basis.
Recipients can then withdraw their earned funds
Technical Details
Architecture Design
This Architecture follows the Beacon proxy & Factory patterns.
The specification contains 4 types of contracts:
Many Proxy Contracts (One Per Campaign Manager)
One Beacon Contract
One Implementation Contract
One Factory Contract that creates proxy contracts
A cron job periodically triggers the proxy contracts to update the balances for each campaign manager.
Terminology:
Owner - administrative Multi-Sig wallet that is the owner of important contracts such as Factory and Proxy and has owner privileges in case of issues.
Worker - an address that will be assigned to server to be able to make earnings updates on behalf of Spindl or the end user to the smart contract. Access can be revoked by the owner.
Manager - An advertiser running campaigns, who is responsible for funding campaigns.
Recipient - in most cases this will be the referrer or referent for the referral campaign. In the context of the smart contract, they are recipients of earnings.
Campaign - Similar to other marketing platforms, we want to distinguish different campaigns by the end goals. Multiple campaigns can live in the same smart contract.
Backend Flow
This is the typical scenario we should be following:
When a new campaign manager (Advertiser) is onboarded, Factory creates a new proxy campaign manager contract using
deployProxy
.When campaign manager is ready, a campaign is created using
createCampaign
and recipients are added usingaddRecipients
Campaign Manager adds funds to campaign using
fundCampaign
to the campaign. Campaign Manager can continue to add more funds later while campaign is ACTIVE.Campaign is set to status ACTIVE by server.
When server calculates attributions, it can update earnings balances for campaigns using
updateBalances
. We can also implicitly create recipient records with this method if a recipient does not existOnce a recipient has new earnings, we can check their balance using
getRecipient
to see the amount. They can then withdraw those usingwithdrawRecipientEarnings
. If a user has pending balances from multiple campaigns, they can usewithdrawManyRecipientEarnings
and pass in the campaigns.Once campaign is complete, Campaign is set to status of COMPLETED. The Campaign Manager cannot add funds to campaign at this point.
If the entire budget of campaign is not spent, then campaign can be set to status COMPLETED by the server. The Campaign Manager will then be able to withdraw (using
withdrawRemainingCampaignBalance
) the difference between totalBalance and totalEarned so we only want to update status to COMPLETED when attribution of earnings is complete.Campaign can be re-started (i.e changed back from COMPLETED to ACTIVE) & be funded by campaign manager again. The reason is that the general desire in this contract is to keep features flexible.
Proxy Contract
This contract shall act as a proxy for the Implementation Contract. It will request the address of the Implementation from Beacon Contract. Once retrieved, it will forward function calls and transactions to the Implementation Contract using delegateCall
, while keeping track of state within itself per campaign manager. Also as a result of this, the transaction will use the state of storage variables present in the Proxy Contract, not the Implementation Contract.
Beacon Contract
Refer to OZ's documentation for the Beacon pattern. This contract holds reference to which implementation contract the proxies should point to. The reason for this approach is that when we want to update 100 proxy contracts to v2 of implementation, we have to only update the beacon contract to point to the v2 implementation & all the proxies get this reference from beacon. The alternative approach we considered was a standard UUPS approach, but in that case we would have to update all the 100 proxies to point to the v2 implementation.
Implementation Contract
The Implementation Contract is where the bulk of the business logic resides. It is the contract that will be upgraded, and it is the contract that will be called by the proxy contract. The Implementation Contract will be deployed once and then upgraded as needed.
It is responsible for having methods that proxy needs to keep track of and update state
Every future Implementation Contract must have the methods to upgrade itself since we are using Upgradeable Beacon pattern. Open Zeppelin upgradable packages should take care of this by default, but please add tests as needed.
Factory Contract
Will have ability to deploy new Proxy contract per Campaign Manager
It is useful to have a centralized contract that deploys so we can index the arbitrary proxies that are created
UX for end-users from Front-end
Advertiser UI Flow
Campaign Manager logins into Spindl App and creates Campaign, specifying the "conversion" events and associated payout information
Spindl's backend creates a new Proxy Contract on-chain, if one that doesn't already exist
Spindl's backend creates a new campaign (
createCampaign
) on-chain with the specified currency (ETH or pre-approved ERC20's)The Campaign Manager funds the campaign
Spindl updates the balances accordingly
Recipient UI Flow
Gets a link from campaign manager if they are invited to be part of referral campaign
Starts advertisting the product & wait for Spindl do add attribution
Can log in with their wallet to recipient dashboard, check their balances & withdraw. (they will pay gas for this)
FAQs
How to remove a recipient?
You cannot remove the mapping of an existing refer. An owner or worker can set the recipient.status = PAUSED with setRecipientStatus
& they will not be able to withdraw money
How to refund an campaign manager if they didn't spend their campaign funds?
At the moment this has to be done manually by owner or front-end using a few steps.
Campaign should be set to status
COMPLETED
so that people cannot add recipients, etc. The attribution of all earnings should be done at this point.Campaign Manager can withdraw the difference between
totalEarned
andtotalBalance
usingwithdrawRemainingCampaignBalance
What validation is in place to ensure balances are correct for recipients & campaigns?
You cannot over-attribute a campaign. For example, in campaign ABC an campaign manager added a totalBalance of $100 to campaign & 2 recipients are added. First campaign manager earned $60 from ABC, then the contract doesn't allow the earned amount of 2nd campaign manager to be more than $40. This prevent over attribution on campaign level.
Also recipient cannot withdraw more than they earned. This prevents recipients withdrawing more than the totalBalance of the campaign since #1 is in place.
If the campaign becomes stale, and recipients don't earn entire balance of campaign, then campaign can be completed & campaign manager can withdraw the difference between totalBalance & totalEarned in campaign. The totalBalance is then updated to reflect this, & campaign manager cannot withdraw again
How can the owner pause the proxy contract?
Owner can set worker address to address(0) to prevent server from making any updates
They then can pause all the campaigns within the campaign manager proxy. This will prevent recipients from withdrawing money or from any balances being updated although campaign manager will still technically be able to add more funds to the campaign
If recipients' address get compromised, during an active campaign, how do we resolve this?
If a recipient loses their private key they will be unable to withdraw any earnings from the contract. The recipient will need to contact the owner to resolve the issue. They will need to first create a new referral account using a different wallet address and have the old account paused. Then they will need to contact the owner to transfer the remaining earnings from the old recipient to the new recipient. The owner will need to verify that the new recipient is the same person as the old recipient.
Note there are no plans to implement a way to transfer earnings from one recipient to another in the contract. This is because of the additional security risks that would be introduced by allowing this. Instead, the owner will need to modify the state of the ledger on the backend servers and wait for another
updateBalances
to be called to update the state of the contract.
How do recipients withdraw their earnings?
They should use
withdrawRecipientEarnings
orwithdrawManyRecipientEarnings
Recipient should be in status
ACTIVE
If Campaign status is
PAUSED
, then recipient cannot withdrawcannot individually withdraw more than difference between earned & already withdrawn
cannot individually withdraw more than total budget of campaign.
How is the server wallet worker
kept secure
We are using MPC wallet technology to prevent and limit surface are of attack.
What are the different Campaign States & what do they do?
State
Description
Can balances be updated?
Can recipient be added?
Can Campaign be funded?
Can recipient withdraw?
Can campaign state be updated in current state?
Can campaign manager withdraw unspent campaign balance?
NONE
null state. campaign hasn't been created yet.
NO
NO
NO
NO
YES (only via
createCampaign
)
NO
CREATED
campaign is created
during createCampaign
.
NO
YES
YES
NO
YES
NO
ACTIVE
campaign can be set active via setCampaignState
YES
YES
YES
YES
YES
NO
PAUSED
campaign is paused to prevent withdrawals and funding
YES
NO
NO
NO
YES
NO
COMPLETED
campaign is successfully completed.
YES
NO
NO
YES
YES
YES
What are the different Recipient States & what do they do?
NONE
null state. recipient is not created yet
NO
ACTIVE
default state after recipient is created
Only if Campaign is ACTIVE
,
COMPLETED
PAUSED
paused to prevent recipient from withdrawing & to potentially sunset them
NO
Last updated