Skip to content

Commit 7a87823

Browse files
authored
Merge pull request #2390 from onflow/robert/contract-removal-permissioning
Separate permission setting of contract deploy/update and removal
2 parents aae8bbb + 8505b6e commit 7a87823

File tree

5 files changed

+228
-58
lines changed

5 files changed

+228
-58
lines changed

Diff for: fvm/blueprints/contracts.go

+31-9
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@ import (
1414
const ContractDeploymentAuthorizedAddressesPathDomain = "storage"
1515
const ContractDeploymentAuthorizedAddressesPathIdentifier = "authorizedAddressesToDeployContracts"
1616

17+
const ContractRemovalAuthorizedAddressesPathDomain = "storage"
18+
const ContractRemovalAuthorizedAddressesPathIdentifier = "authorizedAddressesToRemoveContracts"
19+
1720
const IsContractDeploymentRestrictedPathDomain = "storage"
1821
const IsContractDeploymentRestrictedPathIdentifier = "isContractDeploymentRestricted"
1922

20-
const setContractDeploymentAuthorizersTransactionTemplate = `
23+
const setContractOperationAuthorizersTransactionTemplate = `
2124
transaction(addresses: [Address], path: StoragePath) {
2225
prepare(signer: AuthAccount) {
2326
signer.load<[Address]>(from: path)
@@ -28,24 +31,43 @@ transaction(addresses: [Address], path: StoragePath) {
2831

2932
// SetContractDeploymentAuthorizersTransaction returns a transaction for updating list of authorized accounts allowed to deploy/update contracts
3033
func SetContractDeploymentAuthorizersTransaction(serviceAccount flow.Address, authorized []flow.Address) (*flow.TransactionBody, error) {
31-
arg1, err := jsoncdc.Encode(utils.AddressSliceToCadenceValue(utils.FlowAddressSliceToCadenceAddressSlice(authorized)))
34+
path := cadence.Path{
35+
Domain: ContractDeploymentAuthorizedAddressesPathDomain,
36+
Identifier: ContractDeploymentAuthorizedAddressesPathIdentifier,
37+
}
38+
return setContractAuthorizersTransaction(path, serviceAccount, authorized)
39+
}
40+
41+
// SetContractRemovalAuthorizersTransaction returns a transaction for updating list of authorized accounts allowed to remove contracts
42+
func SetContractRemovalAuthorizersTransaction(serviceAccount flow.Address, authorized []flow.Address) (*flow.TransactionBody, error) {
43+
path := cadence.Path{
44+
Domain: ContractRemovalAuthorizedAddressesPathDomain,
45+
Identifier: ContractRemovalAuthorizedAddressesPathIdentifier,
46+
}
47+
return setContractAuthorizersTransaction(path, serviceAccount, authorized)
48+
}
49+
50+
func setContractAuthorizersTransaction(
51+
path cadence.Path,
52+
serviceAccount flow.Address,
53+
authorized []flow.Address,
54+
) (*flow.TransactionBody, error) {
55+
addresses := utils.FlowAddressSliceToCadenceAddressSlice(authorized)
56+
addressesArg, err := jsoncdc.Encode(utils.AddressSliceToCadenceValue(addresses))
3257
if err != nil {
3358
return nil, err
3459
}
3560

36-
arg2, err := jsoncdc.Encode(cadence.Path{
37-
Domain: ContractDeploymentAuthorizedAddressesPathDomain,
38-
Identifier: ContractDeploymentAuthorizedAddressesPathIdentifier,
39-
})
61+
pathArg, err := jsoncdc.Encode(path)
4062
if err != nil {
4163
return nil, err
4264
}
4365

4466
return flow.NewTransactionBody().
45-
SetScript([]byte(setContractDeploymentAuthorizersTransactionTemplate)).
67+
SetScript([]byte(setContractOperationAuthorizersTransactionTemplate)).
4668
AddAuthorizer(serviceAccount).
47-
AddArgument(arg1).
48-
AddArgument(arg2), nil
69+
AddArgument(addressesArg).
70+
AddArgument(pathArg), nil
4971
}
5072

5173
const setIsContractDeploymentRestrictedTransactionTemplate = `

Diff for: fvm/handler/contract.go

+33-18
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import (
1616
)
1717

1818
type RestrictedDeploymentEnabledFunc func() bool
19-
type AuthorizedAccountsForContractDeploymentFunc func() []common.Address
19+
type AuthorizedAccountsFunc func() []common.Address
2020
type UseContractAuditVoucherFunc func(address runtime.Address, code []byte) (bool, error)
2121

2222
// ContractHandler handles all interaction
@@ -25,11 +25,12 @@ type UseContractAuditVoucherFunc func(address runtime.Address, code []byte) (boo
2525
// only commit them when called so smart contract
2626
// updates can be delayed until end of the tx execution
2727
type ContractHandler struct {
28-
accounts state.Accounts
29-
draftUpdates map[programs.ContractUpdateKey]programs.ContractUpdate
30-
restrictedDeploymentEnabled RestrictedDeploymentEnabledFunc
31-
authorizedAccounts AuthorizedAccountsForContractDeploymentFunc
32-
useContractAuditVoucher UseContractAuditVoucherFunc
28+
accounts state.Accounts
29+
draftUpdates map[programs.ContractUpdateKey]programs.ContractUpdate
30+
restrictedDeploymentEnabled RestrictedDeploymentEnabledFunc
31+
authorizedDeploymentAccounts AuthorizedAccountsFunc
32+
authorizedRemovalAccounts AuthorizedAccountsFunc
33+
useContractAuditVoucher UseContractAuditVoucherFunc
3334
// handler doesn't have to be thread safe and right now
3435
// is only used in a single thread but a mutex has been added
3536
// here to prevent accidental multi-thread use in the future
@@ -38,15 +39,17 @@ type ContractHandler struct {
3839

3940
func NewContractHandler(accounts state.Accounts,
4041
restrictedDeploymentEnabled RestrictedDeploymentEnabledFunc,
41-
authorizedAccounts AuthorizedAccountsForContractDeploymentFunc,
42+
authorizedDeploymentAccounts AuthorizedAccountsFunc,
43+
authorizedRemovalAccounts AuthorizedAccountsFunc,
4244
useContractAuditVoucher UseContractAuditVoucherFunc,
4345
) *ContractHandler {
4446
return &ContractHandler{
45-
accounts: accounts,
46-
draftUpdates: make(map[programs.ContractUpdateKey]programs.ContractUpdate),
47-
restrictedDeploymentEnabled: restrictedDeploymentEnabled,
48-
authorizedAccounts: authorizedAccounts,
49-
useContractAuditVoucher: useContractAuditVoucher,
47+
accounts: accounts,
48+
draftUpdates: make(map[programs.ContractUpdateKey]programs.ContractUpdate),
49+
restrictedDeploymentEnabled: restrictedDeploymentEnabled,
50+
authorizedDeploymentAccounts: authorizedDeploymentAccounts,
51+
authorizedRemovalAccounts: authorizedRemovalAccounts,
52+
useContractAuditVoucher: useContractAuditVoucher,
5053
}
5154
}
5255

@@ -80,7 +83,7 @@ func (h *ContractHandler) SetContract(
8083
return err
8184
}
8285

83-
if !exists && !h.isAuthorized(signingAccounts) {
86+
if !exists && !h.isAuthorizedForDeployment(signingAccounts) {
8487
// check if there's an audit voucher for the contract
8588
voucherAvailable, err := h.useContractAuditVoucher(address, code)
8689
if err != nil {
@@ -115,9 +118,13 @@ func (h *ContractHandler) SetContract(
115118
return nil
116119
}
117120

118-
func (h *ContractHandler) RemoveContract(address runtime.Address, name string, signingAccounts []runtime.Address) (err error) {
121+
func (h *ContractHandler) RemoveContract(
122+
address runtime.Address,
123+
name string,
124+
signingAccounts []runtime.Address,
125+
) (err error) {
119126
// check if authorized
120-
if !h.isAuthorized(signingAccounts) {
127+
if !h.isAuthorizedForRemoval(signingAccounts) {
121128
err = errors.NewOperationAuthorizationErrorf("RemoveContract", "removing contracts requires authorization from specific accounts")
122129
return fmt.Errorf("removing contract failed: %w", err)
123130
}
@@ -205,10 +212,18 @@ func (h *ContractHandler) UpdateKeys() []programs.ContractUpdateKey {
205212
return keys
206213
}
207214

208-
func (h *ContractHandler) isAuthorized(signingAccounts []runtime.Address) bool {
215+
func (h *ContractHandler) isAuthorizedForDeployment(signingAccounts []runtime.Address) bool {
216+
return h.isAuthorized(signingAccounts, h.authorizedDeploymentAccounts)
217+
}
218+
219+
func (h *ContractHandler) isAuthorizedForRemoval(signingAccounts []runtime.Address) bool {
220+
return h.isAuthorized(signingAccounts, h.authorizedRemovalAccounts)
221+
}
222+
223+
func (h *ContractHandler) isAuthorized(signingAccounts []runtime.Address, authorizedAccounts AuthorizedAccountsFunc) bool {
209224
if h.restrictedDeploymentEnabled() {
210-
accs := h.authorizedAccounts()
211-
for _, authorized := range accs {
225+
accts := authorizedAccounts()
226+
for _, authorized := range accts {
212227
for _, signer := range signingAccounts {
213228
if signer == authorized {
214229
// a single authorized singer is enough

Diff for: fvm/handler/contract_test.go

+132-19
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ func TestContract_ChildMergeFunctionality(t *testing.T) {
2626
err := accounts.Create(nil, address)
2727
require.NoError(t, err)
2828

29-
contractHandler := handler.NewContractHandler(accounts, func() bool { return false }, nil, nil)
29+
contractHandler := handler.NewContractHandler(accounts, func() bool { return false }, nil, nil, nil)
3030

3131
// no contract initially
3232
names, err := contractHandler.GetContractNames(rAdd)
@@ -68,35 +68,141 @@ func TestContract_ChildMergeFunctionality(t *testing.T) {
6868
cont, err = contractHandler.GetContract(rAdd, "testContract")
6969
require.NoError(t, err)
7070
require.Equal(t, cont, []byte("ABC"))
71+
72+
// remove
73+
err = contractHandler.RemoveContract(rAdd, "testContract", nil)
74+
require.NoError(t, err)
75+
76+
// contract still there because no commit yet
77+
cont, err = contractHandler.GetContract(rAdd, "testContract")
78+
require.NoError(t, err)
79+
require.Equal(t, cont, []byte("ABC"))
80+
81+
// commit removal
82+
_, err = contractHandler.Commit()
83+
require.NoError(t, err)
84+
85+
// contract should no longer be there
86+
cont, err = contractHandler.GetContract(rAdd, "testContract")
87+
require.NoError(t, err)
88+
require.Equal(t, []byte(nil), cont)
7189
}
7290

7391
func TestContract_AuthorizationFunctionality(t *testing.T) {
7492
sth := state.NewStateHolder(state.NewState(utils.NewSimpleView()))
7593
accounts := state.NewAccounts(sth)
76-
address := flow.HexToAddress("01")
77-
rAdd := runtime.Address(address)
78-
err := accounts.Create(nil, address)
79-
require.NoError(t, err)
8094

81-
unAuthAdd := flow.HexToAddress("02")
82-
unAuthRAdd := runtime.Address(unAuthAdd)
83-
err = accounts.Create(nil, unAuthAdd)
95+
authAdd := flow.HexToAddress("01")
96+
rAdd := runtime.Address(authAdd)
97+
err := accounts.Create(nil, authAdd)
8498
require.NoError(t, err)
8599

86-
contractHandler := handler.NewContractHandler(accounts,
87-
func() bool { return true },
88-
func() []common.Address { return []common.Address{rAdd} },
89-
func(address runtime.Address, code []byte) (bool, error) { return false, nil })
100+
authRemove := flow.HexToAddress("02")
101+
rRemove := runtime.Address(authRemove)
102+
err = accounts.Create(nil, authRemove)
103+
require.NoError(t, err)
90104

91-
// try to set contract by an unAuthRAdd
92-
err = contractHandler.SetContract(rAdd, "testContract1", []byte("ABC"), []common.Address{unAuthRAdd})
93-
require.Error(t, err)
94-
require.False(t, contractHandler.HasUpdates())
105+
authBoth := flow.HexToAddress("03")
106+
rBoth := runtime.Address(authBoth)
107+
err = accounts.Create(nil, authBoth)
108+
require.NoError(t, err)
95109

96-
// set contract by an authorized account
97-
err = contractHandler.SetContract(rAdd, "testContract2", []byte("ABC"), []common.Address{rAdd})
110+
unAuth := flow.HexToAddress("04")
111+
unAuthR := runtime.Address(unAuth)
112+
err = accounts.Create(nil, unAuth)
98113
require.NoError(t, err)
99-
require.True(t, contractHandler.HasUpdates())
114+
115+
makeHandler := func() *handler.ContractHandler {
116+
return handler.NewContractHandler(accounts,
117+
func() bool { return true },
118+
func() []common.Address { return []common.Address{rAdd, rBoth} },
119+
func() []common.Address { return []common.Address{rRemove, rBoth} },
120+
func(address runtime.Address, code []byte) (bool, error) { return false, nil })
121+
}
122+
123+
t.Run("try to set contract with unauthorized account", func(t *testing.T) {
124+
contractHandler := makeHandler()
125+
126+
err = contractHandler.SetContract(rAdd, "testContract1", []byte("ABC"), []common.Address{unAuthR})
127+
require.Error(t, err)
128+
require.False(t, contractHandler.HasUpdates())
129+
})
130+
131+
t.Run("try to set contract with account only authorized for removal", func(t *testing.T) {
132+
contractHandler := makeHandler()
133+
134+
err = contractHandler.SetContract(rAdd, "testContract1", []byte("ABC"), []common.Address{rRemove})
135+
require.Error(t, err)
136+
require.False(t, contractHandler.HasUpdates())
137+
})
138+
139+
t.Run("set contract with account authorized for adding", func(t *testing.T) {
140+
contractHandler := makeHandler()
141+
142+
err = contractHandler.SetContract(rAdd, "testContract2", []byte("ABC"), []common.Address{rAdd})
143+
require.NoError(t, err)
144+
require.True(t, contractHandler.HasUpdates())
145+
})
146+
147+
t.Run("set contract with account authorized for adding and removing", func(t *testing.T) {
148+
contractHandler := makeHandler()
149+
150+
err = contractHandler.SetContract(rAdd, "testContract2", []byte("ABC"), []common.Address{rBoth})
151+
require.NoError(t, err)
152+
require.True(t, contractHandler.HasUpdates())
153+
})
154+
155+
t.Run("try to remove contract with unauthorized account", func(t *testing.T) {
156+
contractHandler := makeHandler()
157+
158+
err = contractHandler.SetContract(rAdd, "testContract1", []byte("ABC"), []common.Address{rAdd})
159+
require.NoError(t, err)
160+
_, err = contractHandler.Commit()
161+
require.NoError(t, err)
162+
163+
err = contractHandler.RemoveContract(unAuthR, "testContract2", []common.Address{unAuthR})
164+
require.Error(t, err)
165+
require.False(t, contractHandler.HasUpdates())
166+
})
167+
168+
t.Run("remove contract account authorized for removal", func(t *testing.T) {
169+
contractHandler := makeHandler()
170+
171+
err = contractHandler.SetContract(rAdd, "testContract1", []byte("ABC"), []common.Address{rAdd})
172+
require.NoError(t, err)
173+
_, err = contractHandler.Commit()
174+
require.NoError(t, err)
175+
176+
err = contractHandler.RemoveContract(rRemove, "testContract2", []common.Address{rRemove})
177+
require.NoError(t, err)
178+
require.True(t, contractHandler.HasUpdates())
179+
})
180+
181+
t.Run("try to remove contract with account only authorized for adding", func(t *testing.T) {
182+
contractHandler := makeHandler()
183+
184+
err = contractHandler.SetContract(rAdd, "testContract1", []byte("ABC"), []common.Address{rAdd})
185+
require.NoError(t, err)
186+
_, err = contractHandler.Commit()
187+
require.NoError(t, err)
188+
189+
err = contractHandler.RemoveContract(rAdd, "testContract2", []common.Address{rAdd})
190+
require.Error(t, err)
191+
require.False(t, contractHandler.HasUpdates())
192+
})
193+
194+
t.Run("remove contract with account authorized for adding and removing", func(t *testing.T) {
195+
contractHandler := makeHandler()
196+
197+
err = contractHandler.SetContract(rAdd, "testContract1", []byte("ABC"), []common.Address{rAdd})
198+
require.NoError(t, err)
199+
_, err = contractHandler.Commit()
200+
require.NoError(t, err)
201+
202+
err = contractHandler.RemoveContract(rBoth, "testContract2", []common.Address{rBoth})
203+
require.NoError(t, err)
204+
require.True(t, contractHandler.HasUpdates())
205+
})
100206
}
101207

102208
func TestContract_DeploymentVouchers(t *testing.T) {
@@ -120,6 +226,9 @@ func TestContract_DeploymentVouchers(t *testing.T) {
120226
func() []common.Address {
121227
return []common.Address{}
122228
},
229+
func() []common.Address {
230+
return []common.Address{}
231+
},
123232
func(address runtime.Address, code []byte) (bool, error) {
124233
if address.String() == addressWithVoucher.String() {
125234
return true, nil
@@ -170,6 +279,9 @@ func TestContract_ContractUpdate(t *testing.T) {
170279
func() []common.Address {
171280
return []common.Address{}
172281
},
282+
func() []common.Address {
283+
return []common.Address{}
284+
},
173285
func(address runtime.Address, code []byte) (bool, error) {
174286
// Ensure the voucher check is only called once,
175287
// for the initial contract deployment,
@@ -234,6 +346,7 @@ func TestContract_DeterministicErrorOnCommit(t *testing.T) {
234346
func() bool { return false },
235347
nil,
236348
nil,
349+
nil,
237350
)
238351

239352
address1 := runtime.Address(flow.HexToAddress("0000000000000001"))

Diff for: fvm/scriptEnv.go

+1
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ func NewScriptEnvironment(
9191
return true
9292
},
9393
func() []common.Address { return []common.Address{} },
94+
func() []common.Address { return []common.Address{} },
9495
func(address runtime.Address, code []byte) (bool, error) { return false, nil })
9596

9697
if fvmContext.BlockHeader != nil {

0 commit comments

Comments
 (0)