Smart Contract

CampaignEscrowV2

A.14aca78d100d2001.CampaignEscrowV2

Valid From

130,767,611

Deployed

6d ago
Feb 21, 2026, 03:02:22 PM UTC

Dependents

7 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 CampaignEscrowV2 {
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        
27        init(
28            id: String,
29            creator: Address,
30            brand: Address,
31            threshold: UFix64,
32            payout: UFix64,
33            deadline: UFix64,
34            createdAt: UFix64,
35            scheduledTxId: String?
36        ) {
37            self.id = id
38            self.creator = creator
39            self.brand = brand
40            self.threshold = threshold
41            self.payout = payout
42            self.deadline = deadline
43            self.createdAt = createdAt
44            self.scheduledTxId = scheduledTxId
45            self.totalScore = 0.0
46            self.creatorScores = {}
47        }
48        
49        // Setter function to add to total score
50        access(all) fun addToTotalScore(_ amount: UFix64) {
51            self.totalScore = self.totalScore + amount
52        }
53        
54        // Getter for totalScore
55        access(all) fun getTotalScore(): UFix64 {
56            return self.totalScore
57        }
58    }
59    
60    // Oracle account for triggering payouts
61    access(all) let oracle: Address
62    
63    // Storage for campaigns and FLOW vault
64    access(all) var campaigns: {String: Campaign}
65    access(all) var vault: @FlowToken.Vault
66    
67    // Create a new campaign with FLOW deposit
68    access(all) fun createCampaign(
69        id: String,
70        creator: Address,
71        threshold: UFix64,
72        payout: UFix64,
73        deadline: UFix64,
74        from: @FlowToken.Vault
75    ): Bool {
76        // Validate inputs
77        pre {
78            threshold > 0.0: "Threshold must be positive"
79            payout > 0.0: "Payout must be positive"
80            deadline > getCurrentBlock().timestamp: "Deadline must be in the future"
81            !self.campaigns.containsKey(id): "Campaign ID already exists"
82        }
83        
84        // Create campaign
85        let campaign = Campaign(
86            id: id,
87            creator: creator,
88            brand: self.oracle, // Using oracle as brand for simplicity
89            threshold: threshold,
90            payout: payout,
91            deadline: deadline,
92            createdAt: getCurrentBlock().timestamp,
93            scheduledTxId: nil
94        )
95        
96        // Store campaign
97        self.campaigns[id] = campaign
98        
99        // Deposit FLOW into vault
100        self.vault.deposit(from: <- from)
101        
102        // Emit event
103        emit CampaignCreated(id: id, creator: creator, threshold: threshold, payout: payout)
104        
105        return true
106    }
107    
108    // Update creator performance score (oracle only)
109    access(all) fun updateCreatorScore(
110        campaignId: String,
111        creator: Address,
112        score: UFix64,
113        signer: Address
114    ): Bool {
115        pre {
116            signer == self.oracle: "Only oracle can update scores"
117            self.campaigns.containsKey(campaignId): "Campaign does not exist"
118        }
119        
120        let campaign = self.campaigns[campaignId]!
121        
122        // Update creator score
123        campaign.creatorScores[creator] = score
124        
125        // Update total campaign score using setter
126        campaign.addToTotalScore(score)
127        
128        // Write back the mutated struct to storage
129        self.campaigns[campaignId] = campaign
130        
131        // Emit event
132        emit CreatorScoreUpdated(campaignId: campaignId, creator: creator, score: score)
133        
134        return true
135    }
136    
137    // Verify campaign performance and trigger payout or refund
138    access(all) fun verifyAndPayout(
139        campaignId: String,
140        signer: Address
141    ): String {
142        pre {
143            signer == self.oracle: "Only oracle can verify and payout"
144            self.campaigns.containsKey(campaignId): "Campaign does not exist"
145        }
146        
147        let campaign = self.campaigns[campaignId]!
148        
149        // Check if deadline has passed
150        if getCurrentBlock().timestamp > campaign.deadline {
151            // Campaign expired - check if threshold was met
152            if campaign.getTotalScore() >= campaign.threshold {
153                return "payout"
154            } else {
155                return "refund"
156            }
157        } else {
158            // Campaign still active - check if threshold already met
159            if campaign.getTotalScore() >= campaign.threshold {
160                return "payout"
161            } else {
162                return "pending"
163            }
164        }
165    }
166    
167    // Trigger payout if KPI is met (oracle only)
168    access(all) fun triggerPayout(
169        campaignId: String,
170        signer: Address
171    ): Bool {
172        pre {
173            signer == self.oracle: "Only oracle can trigger payouts"
174            self.campaigns.containsKey(campaignId): "Campaign does not exist"
175        }
176        
177        let campaign = self.campaigns[campaignId]!
178        
179        // Check if KPI is met
180        if campaign.getTotalScore() >= campaign.threshold {
181            // Calculate creator shares and distribute payouts
182            let totalScore = campaign.getTotalScore()
183            let payoutAmount = campaign.payout
184            
185            // Guard against division by zero
186            if totalScore > 0.0 {
187                // Distribute to each creator based on their score share
188                for creator in campaign.creatorScores.keys {
189                    let creatorScore = campaign.creatorScores[creator]!
190                    let creatorShare = (creatorScore / totalScore) * payoutAmount
191                
192                    // Transfer FLOW to creator
193                    // Note: In a real implementation, you'd need to handle the transfer
194                    // This is simplified for the demo
195                }
196            }
197            
198            // Emit event
199            emit PayoutTriggered(campaignId: campaignId, totalScore: campaign.getTotalScore(), payout: payoutAmount)
200            
201            return true
202        }
203        
204        return false
205    }
206    
207    // Trigger refund if campaign failed (oracle only)
208    access(all) fun triggerRefund(
209        campaignId: String,
210        signer: Address
211    ): Bool {
212        pre {
213            signer == self.oracle: "Only oracle can trigger refunds"
214            self.campaigns.containsKey(campaignId): "Campaign does not exist"
215        }
216        
217        let campaign = self.campaigns[campaignId]!
218        
219        // Check if deadline has passed and KPI not met
220        if getCurrentBlock().timestamp > campaign.deadline && campaign.getTotalScore() < campaign.threshold {
221            // Refund FLOW to brand
222            // Note: In a real implementation, you'd need to handle the transfer
223            // This is simplified for the demo
224            
225            // Emit event
226            emit CampaignRefunded(campaignId: campaignId, brand: campaign.brand, amount: campaign.payout)
227            
228            return true
229        }
230        
231        return false
232    }
233    
234    // Get campaign details
235    access(all) fun getCampaign(id: String): Campaign? {
236        return self.campaigns[id]
237    }
238    
239    // Get all campaigns
240    access(all) fun getAllCampaigns(): [Campaign] {
241        let campaigns: [Campaign] = []
242        for campaign in self.campaigns.values {
243            campaigns.append(campaign)
244        }
245        return campaigns
246    }
247    
248    // Get campaigns by creator
249    access(all) fun getCampaignsByCreator(creator: Address): [Campaign] {
250        let result: [Campaign] = []
251        for campaign in self.campaigns.values {
252            if campaign.creator == creator {
253                result.append(campaign)
254            }
255        }
256        return result
257    }
258    
259    // Events
260    access(all) event CampaignCreated(id: String, creator: Address, threshold: UFix64, payout: UFix64)
261    access(all) event CreatorScoreUpdated(campaignId: String, creator: Address, score: UFix64)
262    access(all) event PayoutTriggered(campaignId: String, totalScore: UFix64, payout: UFix64)
263    access(all) event CampaignRefunded(campaignId: String, brand: Address, amount: UFix64)
264    
265    init(oracle: Address) {
266        self.oracle = oracle
267        self.campaigns = {}
268        self.vault <- FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>())
269    }
270}