Smart Contract
CampaignEscrowV3
A.14aca78d100d2001.CampaignEscrowV3
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}