Skip to content

Commit ea1b738

Browse files
authored
Contract Audit Vouchers Merge (#271)
* initial proposal * update generated files * ci fix * ci fix * downgrade go version for ci * go mod tidy * add comments * generate for ci * refactor code * minor refactor
1 parent e014b23 commit ea1b738

File tree

19 files changed

+3210
-0
lines changed

19 files changed

+3210
-0
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,14 @@ These contracts manage the epoch functionality of Flow, the mechanism by which F
124124
`FlowClusterQC.cdc` and `FlowDKG.cdc` manage processes specific to collector and consensus nodes, respectively.
125125
`FlowEpoch.cdc` ties all of the epoch and staking contracts together into a coherent state machine that will run on its own.
126126

127+
## Flow Contract Audits
128+
129+
`contracts/FlowContractAudits.cdc`
130+
131+
This contract contains a list of contract audit vouchers used for contract deployment. If enabled, on contract deployment the FVM will check the code hash
132+
and target account against the list of vouchers on this contract.
133+
The service account can authorize auditors to add/remove items to/from the list on this contract.
134+
127135
# Testing
128136

129137
To run the tests in the repo, use `make test`.

contracts/FlowContractAudits.cdc

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
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+
}

lib/go/Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
test:
33
$(MAKE) test -C contracts
44
$(MAKE) test -C test
5+
$(MAKE) test -C test/ContractAuditVouchers
56

67
.PHONY: generate
78
generate:
@@ -13,3 +14,5 @@ ci:
1314
$(MAKE) ci -C contracts
1415
$(MAKE) ci -C templates
1516
$(MAKE) ci -C test
17+
$(MAKE) ci -C test/ContractAuditVouchers
18+

0 commit comments

Comments
 (0)