1
+ pub contract FlowContractAudits {
2
+
3
+ // Event that is emitted when a new Auditor resource is created
4
+ pub event AuditorCreated ()
5
+
6
+ // Event that is emitted when a new contract audit voucher is created
7
+ pub event VoucherCreated (address : Address ?, recurrent : Bool , expiryBlockHeight : UInt64 ?, codeHash : String )
8
+
9
+ // Event that is emitted when a contract audit voucher is used
10
+ pub event VoucherUsed (address : Address , key : String , recurrent : Bool , expiryBlockHeight : UInt64 ?)
11
+
12
+ // Event that is emitted when a contract audit voucher is removed
13
+ pub event VoucherRemoved (key : String , recurrent : Bool , expiryBlockHeight : UInt64 ?)
14
+
15
+ // Dictionary of all vouchers
16
+ access (contract) var vouchers : {String : AuditVoucher }
17
+
18
+ // The storage path for the admin resource
19
+ pub let AdminStoragePath : StoragePath
20
+
21
+ // The storage Path for auditors' AuditorProxy
22
+ pub let AuditorProxyStoragePath : StoragePath
23
+
24
+ // The public path for auditors' AuditorProxy capability
25
+ pub let AuditorProxyPublicPath : PublicPath
26
+
27
+ // Single audit voucher that is used for contract deployment
28
+ pub struct AuditVoucher {
29
+
30
+ // Address of the account the voucher is intended for
31
+ // If nil, the contract can be deployed to any account
32
+ pub let address : Address ?
33
+
34
+ // If false, the voucher will be removed after first use
35
+ pub let recurrent : Bool
36
+
37
+ // If non-nil, the voucher won't be valid after the expiry block height
38
+ pub let expiryBlockHeight : UInt64 ?
39
+
40
+ // Hash of contract code
41
+ pub let codeHash : String
42
+
43
+ init (address : Address ?, recurrent : Bool , expiryBlockHeight : UInt64 ?, codeHash : String ) {
44
+ self .address = address
45
+ self .recurrent = recurrent
46
+ self .expiryBlockHeight = expiryBlockHeight
47
+ self .codeHash = codeHash
48
+ }
49
+ }
50
+
51
+ // Returns all current vouchers
52
+ pub fun getAllVouchers (): {String : AuditVoucher } {
53
+ return self .vouchers
54
+ }
55
+
56
+ // Get the associated dictionary key for given address and codeHash
57
+ pub fun generateVoucherKey (address : Address ?, codeHash : String ): String {
58
+ if address ! = nil {
59
+ return address! .toString ().concat (" -" ).concat (codeHash)
60
+ }
61
+ return " any-" .concat (codeHash)
62
+ }
63
+
64
+ pub fun hashContractCode (_ code : String ): String {
65
+ return String.encodeHex (HashAlgorithm.SHA3_256.hash (code.utf8))
66
+ }
67
+
68
+ // Auditors can create new vouchers and remove them
69
+ pub resource Auditor {
70
+
71
+ // Create new voucher
72
+ pub fun addVoucher (address : Address ?, recurrent : Bool , expiryOffset : UInt64 ?, code : String ) {
73
+
74
+ // calculate expiry block height based on expiryOffset
75
+ var expiryBlockHeight : UInt64 ? = nil
76
+ if expiryOffset ! = nil {
77
+ expiryBlockHeight = getCurrentBlock ().height + expiryOffset!
78
+ }
79
+
80
+ let codeHash = FlowContractAudits.hashContractCode (code)
81
+ let key = FlowContractAudits.generateVoucherKey (address : address, codeHash : codeHash)
82
+
83
+ // if a voucher with the same key exists, remove it first
84
+ FlowContractAudits.deleteVoucher (key)
85
+
86
+ let voucher = AuditVoucher (address : address, recurrent : recurrent, expiryBlockHeight : expiryBlockHeight, codeHash : codeHash)
87
+
88
+ FlowContractAudits.vouchers.insert (key : key, voucher)
89
+
90
+ emit VoucherCreated (address : address, recurrent : recurrent, expiryBlockHeight : expiryBlockHeight, codeHash : codeHash)
91
+ }
92
+
93
+ // Remove a voucher with given key
94
+ pub fun deleteVoucher (key : String ) {
95
+ FlowContractAudits.deleteVoucher (key)
96
+ }
97
+ }
98
+
99
+ // Used by admin to set the Auditor capability
100
+ pub resource interface AuditorProxyPublic {
101
+ pub fun setAuditorCapability (_ cap : Capability <&Auditor >)
102
+ }
103
+
104
+ // The auditor account will have audit access through AuditorProxy
105
+ // This enables the admin account to revoke access
106
+ // See https://docs.onflow.org/cadence/design-patterns/#capability-revocation
107
+ pub resource AuditorProxy : AuditorProxyPublic {
108
+ access (self ) var auditorCapability : Capability <&Auditor >?
109
+
110
+ pub fun setAuditorCapability (_ cap : Capability <&Auditor >) {
111
+ self .auditorCapability = cap
112
+ }
113
+
114
+ pub fun addVoucher (address : Address ?, recurrent : Bool , expiryOffset : UInt64 ?, code : String ) {
115
+ self .auditorCapability! .borrow ()! .addVoucher (address : address, recurrent : recurrent, expiryOffset : expiryOffset, code : code)
116
+ }
117
+
118
+ pub fun deleteVoucher (key : String ) {
119
+ self .auditorCapability! .borrow ()! .deleteVoucher (key : key)
120
+ }
121
+
122
+ init () {
123
+ self .auditorCapability = nil
124
+ }
125
+
126
+ }
127
+
128
+ // Can be called by anyone but needs a capability to function
129
+ pub fun createAuditorProxy (): @AuditorProxy {
130
+ return <- create AuditorProxy ()
131
+ }
132
+
133
+ pub resource Administrator {
134
+
135
+ // Creates new Auditor
136
+ pub fun createNewAuditor (): @Auditor {
137
+ emit AuditorCreated ()
138
+ return <- create Auditor ()
139
+ }
140
+
141
+ // Checks all vouchers and removes expired ones
142
+ pub fun cleanupExpiredVouchers () {
143
+ for key in FlowContractAudits.vouchers.keys {
144
+ let v = FlowContractAudits.vouchers[key]!
145
+ if v.expiryBlockHeight ! = nil {
146
+ if getCurrentBlock ().height > v.expiryBlockHeight! {
147
+ FlowContractAudits.deleteVoucher (key)
148
+ }
149
+ }
150
+ }
151
+ }
152
+
153
+ // For testing
154
+ pub fun useVoucherForDeploy (address : Address , code : String ): Bool {
155
+ return FlowContractAudits.useVoucherForDeploy (address : address, code : code)
156
+ }
157
+ }
158
+
159
+ // This function will be called by the FVM on contract deploy/update
160
+ access (contract) fun useVoucherForDeploy (address : Address , code : String ): Bool {
161
+ let codeHash = FlowContractAudits.hashContractCode (code)
162
+ var key = FlowContractAudits.generateVoucherKey (address : address, codeHash : codeHash)
163
+
164
+ // first check for voucher based on target account
165
+ // if not found check for any account
166
+ if ! FlowContractAudits.vouchers.containsKey (key) {
167
+ key = FlowContractAudits.generateVoucherKey (address : nil , codeHash : codeHash)
168
+ if ! FlowContractAudits.vouchers.containsKey (key) {
169
+ return false
170
+ }
171
+ }
172
+
173
+ let v = FlowContractAudits.vouchers[key]!
174
+
175
+ // ensure contract code matches the voucher
176
+ if v.codeHash ! = codeHash {
177
+ return false
178
+ }
179
+
180
+ // if expiryBlockHeight is set, check the current block height
181
+ // and remove/expire the voucher if not within the acceptable range
182
+ if v.expiryBlockHeight ! = nil {
183
+ if getCurrentBlock ().height > v.expiryBlockHeight! {
184
+ FlowContractAudits.deleteVoucher (key)
185
+ return false
186
+ }
187
+ }
188
+
189
+ // remove the voucher if not recurrent
190
+ if ! v.recurrent {
191
+ FlowContractAudits.deleteVoucher (key)
192
+ }
193
+
194
+ emit VoucherUsed (address : address, key : key, recurrent : v.recurrent, expiryBlockHeight : v.expiryBlockHeight)
195
+ return true
196
+ }
197
+
198
+ // Helper function to remove a voucher with given key
199
+ access (contract) fun deleteVoucher (_ key : String ) {
200
+ let v = FlowContractAudits.vouchers.remove (key : key)
201
+ if v ! = nil {
202
+ emit VoucherRemoved (key : key, recurrent : v! .recurrent, expiryBlockHeight : v! .expiryBlockHeight)
203
+ }
204
+ }
205
+
206
+ init () {
207
+ self .vouchers = {}
208
+
209
+ self .AdminStoragePath = / storage/ flowContractAuditVouchersAdmin
210
+ self .AuditorProxyStoragePath = / storage/ flowContractAuditVouchersAuditorProxy
211
+ self .AuditorProxyPublicPath = / public/ flowContractAuditVouchersAuditorProxy
212
+
213
+ let admin <- create Administrator ()
214
+ self .account.save (<- admin, to : self .AdminStoragePath)
215
+ }
216
+ }
0 commit comments