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