Smart Contract

CampaignEscrowV3

A.14aca78d100d2001.CampaignEscrowV3

Valid From

130,774,926

Deployed

1w ago
Feb 21, 2026, 03:07:52 PM UTC

Dependents

6 imports
1/**
2 * CampaignEscrow Contract
3 * 
4 * Manages campaign escrow with FLOW tokens and automated payouts
5 * Integrates with Flow Forte scheduled transactions for auto-refund
6 * Handles trustless campaign management between brands and creators
7 */
8
9import FungibleToken from 0xf233dcee88fe0abe
10import FlowToken from 0x1654653399040a61
11
12access(all) contract CampaignEscrowV3 {
13    
14    // Campaign data structure
15    access(all) struct Campaign {
16        access(all) let id: String
17        access(all) let creator: Address
18        access(all) let brand: Address
19        access(all) let threshold: UFix64
20        access(all) let payout: UFix64
21        access(all) let deadline: UFix64
22        access(all) let createdAt: UFix64
23        access(all) let scheduledTxId: String? // Forte scheduled transaction ID for auto-refund
24        access(self) var totalScore: UFix64
25        access(all) var creatorScores: {Address: UFix64}
26        access(self) var paidOut: Bool
27        
28        init(
29            id: String,
30            creator: Address,
31            brand: Address,
32            threshold: UFix64,
33            payout: UFix64,
34            deadline: UFix64,
35            createdAt: UFix64,
36            scheduledTxId: String?
37        ) {
38            self.id = id
39            self.creator = creator
40            self.brand = brand
41            self.threshold = threshold
42            self.payout = payout
43            self.deadline = deadline
44            self.createdAt = createdAt
45            self.scheduledTxId = scheduledTxId
46            self.totalScore = 0.0
47            self.creatorScores = {}
48            self.paidOut = false
49        }
50        
51        // Setter function to add to total score
52        access(all) fun addToTotalScore(_ amount: UFix64) {
53            self.totalScore = self.totalScore + amount
54        }
55        
56        // Getter for totalScore
57        access(all) fun getTotalScore(): UFix64 {
58            return self.totalScore
59        }
60        
61        // Setter for paidOut
62        access(all) fun setPaidOut(_ value: Bool) {
63            self.paidOut = value
64        }
65        
66        // Getter for paidOut
67        access(all) fun isPaidOut(): Bool {
68            return self.paidOut
69        }
70    }
71    
72    // Oracle account for triggering payouts
73    access(all) let oracle: Address
74    
75    // Storage for campaigns and FLOW vault
76    access(all) var campaigns: {String: Campaign}
77    access(all) var vault: @FlowToken.Vault
78    
79    // Create a new campaign with FLOW deposit
80    access(all) fun createCampaign(
81        id: String,
82        creator: Address,
83        threshold: UFix64,
84        payout: UFix64,
85        deadline: UFix64,
86        from: @FlowToken.Vault
87    ): Bool {
88        // Validate inputs
89        pre {
90            threshold > 0.0: "Threshold must be positive"
91            payout > 0.0: "Payout must be positive"
92            deadline > getCurrentBlock().timestamp: "Deadline must be in the future"
93            !self.campaigns.containsKey(id): "Campaign ID already exists"
94        }
95        
96        // Create campaign
97        let campaign = Campaign(
98            id: id,
99            creator: creator,
100            brand: self.oracle, // Using oracle as brand for simplicity
101            threshold: threshold,
102            payout: payout,
103            deadline: deadline,
104            createdAt: getCurrentBlock().timestamp,
105            scheduledTxId: nil
106        )
107        
108        // Store campaign
109        self.campaigns[id] = campaign
110        
111        // Deposit FLOW into vault
112        self.vault.deposit(from: <- from)
113        
114        // Emit event
115        emit CampaignCreated(id: id, creator: creator, threshold: threshold, payout: payout)
116        
117        return true
118    }
119    
120    // Update creator performance score (oracle only)
121    access(all) fun updateCreatorScore(
122        campaignId: String,
123        creator: Address,
124        score: UFix64,
125        signer: Address
126    ): Bool {
127        pre {
128            signer == self.oracle: "Only oracle can update scores"
129            self.campaigns.containsKey(campaignId): "Campaign does not exist"
130        }
131        
132        let campaign = self.campaigns[campaignId]!
133        
134        // Update creator score
135        campaign.creatorScores[creator] = score
136        
137        // Update total campaign score using setter
138        campaign.addToTotalScore(score)
139        
140        // Write back the mutated struct to storage
141        self.campaigns[campaignId] = campaign
142        
143        // Emit event
144        emit CreatorScoreUpdated(campaignId: campaignId, creator: creator, score: score)
145        
146        return true
147    }
148    
149    // Verify campaign performance and trigger payout or refund
150    access(all) fun verifyAndPayout(
151        campaignId: String,
152        signer: Address
153    ): String {
154        pre {
155            signer == self.oracle: "Only oracle can verify and payout"
156            self.campaigns.containsKey(campaignId): "Campaign does not exist"
157        }
158        
159        let campaign = self.campaigns[campaignId]!
160        
161        // Check if deadline has passed
162        if getCurrentBlock().timestamp > campaign.deadline {
163            // Campaign expired - check if threshold was met
164            if campaign.getTotalScore() >= campaign.threshold {
165                return "payout"
166            } else {
167                return "refund"
168            }
169        } else {
170            // Campaign still active - check if threshold already met
171            if campaign.getTotalScore() >= campaign.threshold {
172                return "payout"
173            } else {
174                return "pending"
175            }
176        }
177    }
178    
179    // Trigger payout if KPI is met (oracle only)
180    access(all) fun triggerPayout(
181        campaignId: String,
182        signer: Address
183    ): Bool {
184        pre {
185            signer == self.oracle: "Only oracle can trigger payouts"
186            self.campaigns.containsKey(campaignId): "Campaign does not exist"
187        }
188        
189        var campaign = self.campaigns[campaignId]!
190        
191        // Check if already paid out
192        assert(!campaign.isPaidOut(), message: "Campaign already paid out")
193        
194        // Check if KPI is met
195        if campaign.getTotalScore() >= campaign.threshold {
196            // Calculate creator shares and distribute payouts
197            let totalScore = campaign.getTotalScore()
198            let payoutAmount = campaign.payout
199            
200            // Guard against division by zero
201            if totalScore > 0.0 {
202                // Distribute to each creator based on their score share
203                for creator in campaign.creatorScores.keys {
204                    let creatorScore = campaign.creatorScores[creator]!
205                    let creatorShare = (creatorScore / totalScore) * payoutAmount
206                
207                    // Withdraw from escrow vault
208                    let payment <- self.vault.withdraw(amount: creatorShare) as! @FlowToken.Vault
209                    
210                    // Get creator's FlowToken receiver capability
211                    let receiverCap = getAccount(creator)
212                        .capabilities.get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver)
213                    
214                    let receiver = receiverCap.borrow()
215                        ?? panic("Could not borrow receiver for creator")
216                    
217                    // Deposit FLOW to creator
218                    receiver.deposit(from: <-payment)
219                }
220            }
221            
222            // Mark as paid out
223            campaign.setPaidOut(true)
224            self.campaigns[campaignId] = campaign
225            
226            // Emit event
227            emit PayoutTriggered(campaignId: campaignId, totalScore: campaign.getTotalScore(), payout: payoutAmount)
228            
229            return true
230        }
231        
232        return false
233    }
234    
235    // Trigger refund if campaign failed (oracle only)
236    access(all) fun triggerRefund(
237        campaignId: String,
238        signer: Address
239    ): Bool {
240        pre {
241            signer == self.oracle: "Only oracle can trigger refunds"
242            self.campaigns.containsKey(campaignId): "Campaign does not exist"
243        }
244        
245        var campaign = self.campaigns[campaignId]!
246        
247        // Check if already paid out
248        assert(!campaign.isPaidOut(), message: "Campaign already paid out or refunded")
249        
250        // Check if deadline has passed and KPI not met
251        if getCurrentBlock().timestamp > campaign.deadline && campaign.getTotalScore() < campaign.threshold {
252            // Withdraw full payout amount from escrow
253            let refund <- self.vault.withdraw(amount: campaign.payout) as! @FlowToken.Vault
254            
255            // Get brand's FlowToken receiver capability
256            let receiverCap = getAccount(campaign.brand)
257                .capabilities.get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver)
258            
259            let receiver = receiverCap.borrow()
260                ?? panic("Could not borrow receiver for brand")
261            
262            // Deposit FLOW back to brand
263            receiver.deposit(from: <-refund)
264            
265            // Mark as paid out (refunded)
266            campaign.setPaidOut(true)
267            self.campaigns[campaignId] = campaign
268            
269            // Emit event
270            emit CampaignRefunded(campaignId: campaignId, brand: campaign.brand, amount: campaign.payout)
271            
272            return true
273        }
274        
275        return false
276    }
277    
278    // Get campaign details
279    access(all) fun getCampaign(id: String): Campaign? {
280        return self.campaigns[id]
281    }
282    
283    // Get all campaigns
284    access(all) fun getAllCampaigns(): [Campaign] {
285        let campaigns: [Campaign] = []
286        for campaign in self.campaigns.values {
287            campaigns.append(campaign)
288        }
289        return campaigns
290    }
291    
292    // Get campaigns by creator
293    access(all) fun getCampaignsByCreator(creator: Address): [Campaign] {
294        let result: [Campaign] = []
295        for campaign in self.campaigns.values {
296            if campaign.creator == creator {
297                result.append(campaign)
298            }
299        }
300        return result
301    }
302    
303    // Events
304    access(all) event CampaignCreated(id: String, creator: Address, threshold: UFix64, payout: UFix64)
305    access(all) event CreatorScoreUpdated(campaignId: String, creator: Address, score: UFix64)
306    access(all) event PayoutTriggered(campaignId: String, totalScore: UFix64, payout: UFix64)
307    access(all) event CampaignRefunded(campaignId: String, brand: Address, amount: UFix64)
308    
309    init(oracle: Address) {
310        self.oracle = oracle
311        self.campaigns = {}
312        self.vault <- FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>())
313    }
314}