@@ -9,14 +9,14 @@ Using the factory pattern on Concordium
9
9
This guide makes use of features that are first available in protocol version 7,
10
10
namely getting the contract name and module reference of a smart contract instance
11
11
from a smart contract.
12
- Before the protocol upgrade to version 7, these features will not work.
12
+ Before the protocol upgrade to version 7 (expected summer 2024) , these features will not work.
13
13
14
14
The factory pattern is a design pattern where one contract (the factory) creates instances of
15
15
another contract (the products). This pattern makes sense on Ethereum, where deploying a smart
16
16
contract typically means deploying the code and instantiating a contract in a single transaction.
17
17
This means that creating a new instance of a smart contract typically means redeploying the code,
18
18
even if it is identical to an already-deployed smart contract. This can be undesirable:
19
- redeploying the same code repeatedly is a waste of resources. The factory pattern provides a
19
+ redeploying the same code repeatedly wastes resources. The factory pattern provides a
20
20
workaround for this: the contract code is deployed once, creating a factory contract, which then
21
21
can be invoked to create further (product) contracts.
22
22
@@ -35,7 +35,7 @@ redeploying the code. This eliminates one of the key motivations for using a fac
35
35
to create?
36
36
37
37
This is where the factory pattern comes in. In the case of the factory class pattern,
38
- the code is parametrised by a `DatabaseConnectionFactory ` object, which provides a
38
+ the code is parametrized by a `DatabaseConnectionFactory ` object, which provides a
39
39
method for constructing `DatabaseConnection ` objects. The `DatabaseConnectionFactory `
40
40
interface would be implemented by `MySqlDatabaseConnectionFactory ` and
41
41
`SQLiteDatabaseConnectionFactory ` (and potentially others). Which specific database connection
@@ -48,7 +48,7 @@ redeploying the code. This eliminates one of the key motivations for using a fac
48
48
The point of this is that in object-oriented languages, the factory pattern is solving a
49
49
particular problem: creating instances of a type that is not fully determined. While it may
50
50
be idiomatic in one language, it may not be in another. For instance, in Rust the same code
51
- could be parametrised by a generic type that implements a `DatabaseConnection ` trait, which
51
+ could be parametrized by a generic type that implements a `DatabaseConnection ` trait, which
52
52
provides a `new ` function for constructing `DatabaseConnection ` instances. The particular
53
53
type of database connection that is created would depend on how the generic type parameter
54
54
is eventually instantiated.
@@ -63,24 +63,24 @@ relationship with the products that it produces. In particular, the factory coul
63
63
index of product instances, or ensure that each instance is created distinctly from the others.
64
64
It could also play a role in logging events signalling the creation of new products.
65
65
66
- The sequence diagram below illustrates an idealised factory pattern.
66
+ The sequence diagram below illustrates an idealized factory pattern.
67
67
The user invokes the ``produce `` endpoint on a ``Factory `` contract.
68
68
The factory constructs a new instance of the ``product `` smart contract, invoking the
69
69
``init_product `` constructor.
70
70
Finally, a reference to the new product instance is returned.
71
71
72
72
.. image :: images/ideal-factory.svg
73
- :alt: sequence diagram showing the idealised interaction for the factory pattern
73
+ :alt: sequence diagram showing the idealized interaction for the factory pattern
74
74
75
75
76
76
Unfortunately, implementing a factory pattern on Concordium is complicated by the fact that one
77
77
smart contract instance cannot create other smart contract instances programmatically.
78
78
On Concordium, every smart contract instance is created by a top-level transaction. To achieve
79
- something resembling the factory pattern, we have to separately create the new product instances
80
- in an uninitialized state, and then have the factory initialize them.
79
+ something resembling the factory pattern, the product instances must first be created in an
80
+ uninitialized state, and then the factory must be invoked to initialize them in a separate step .
81
81
82
- The sequence diagram below illustrates how the factory pattern is realised on Concordium.
83
- First of all , the user constructs a new instance of the ``product `` smart contract, invoking the
82
+ The sequence diagram below illustrates how the factory pattern is realized on Concordium.
83
+ First, the user constructs a new instance of the ``product `` smart contract, invoking the
84
84
``init_product `` constructor.
85
85
This creates the ``Product `` contract in a special uninitialized state.
86
86
The user then invokes the ``produce `` endpoint on the ``Factory `` contract, passing in a reference
@@ -91,7 +91,7 @@ the ``Product`` being fully initialized.
91
91
.. image :: images/concordium-factory.svg
92
92
:alt: sequence diagram showing how the factory pattern might be realised on Concordium
93
93
94
- This process is significantly more complex than the idealised factory pattern we started with .
94
+ This process is significantly more complex than the original idealized factory pattern.
95
95
In particular, the user is required to sign two separate transactions, with the second one depending
96
96
on the result of the first one. If this two-step process is presented to the end user, it is likely
97
97
to cause confusion. Additionally, two-step process means that the smart contracts have to be robust
@@ -112,7 +112,7 @@ If the factory does not maintain an on-going relationship with the product,
112
112
then it is generally possible to simply initialize the product entirely in
113
113
the constructor (``init_product ``), removing the separate ``initialize `` operation entirely.
114
114
In general, if the factory contract does not need to update its state
115
- after initializing the product, then it serves no purpose. It would be
115
+ when initializing the product, then it serves no purpose. It would be
116
116
sufficient for the user to construct the product directly in the same way as the factory
117
117
would have.
118
118
@@ -137,7 +137,7 @@ each product.
137
137
138
138
For instance, a CIS2 contract can manage multiple NFTs that each have distinct token IDs.
139
139
Rather than each NFT being its own contract instance (created by a factory), they are simply
140
- handled as part of the state of the overall CIS2 contract. Here, the token ID actts as a
140
+ handled as part of the state of the overall CIS2 contract. Here, the token ID acts as a
141
141
virtual address.
142
142
143
143
The main disadvantage of this approach is that the isolation between the states of each product
@@ -172,21 +172,21 @@ The ``produce`` endpoint
172
172
^^^^^^^^^^^^^^^^^^^^^^^^
173
173
174
174
The ``produce `` method of the factory expects one parameter that is the address of an uninitialized
175
- instance of the ``product `` contract. First of all , the parameter is read from the context:
175
+ instance of the ``product `` contract. First, the parameter is read from the context:
176
176
177
177
.. code-block :: Rust
178
178
179
179
let product_address = ctx.parameter_cursor().get()?;
180
180
181
- The factory needs to be sure that the address does actually refer to an instance of the ``product ``
181
+ The factory needs to be sure that the address actually refers to an instance of the ``product ``
182
182
contract, in order to ensure correct behavior. This can be achieved by checking the module reference
183
183
and contract name against expected values. Together, the module reference and contract name uniquely
184
184
identify the code of the smart contract instance.
185
185
186
186
Getting the module reference and contract name is done using the host functions
187
- ``contract_module_reference `` and ``contract_name ``, respectively. Note: both of these functions
188
- are introduced in protocol version 7, and will not work while the chain is running an earlier
189
- protocol version.
187
+ ``contract_module_reference `` and ``contract_name ``, respectively. ** Note: both of these functions **
188
+ ** are introduced in protocol version 7, and will not work while the chain is running an earlier **
189
+ ** protocol version. **
190
190
191
191
In this example, the factory and product contracts are defined in the same module.
192
192
Thus, to check that the module reference of the product is correct, it is sufficient to check that
@@ -208,23 +208,32 @@ module rereference for the product is to be determined later, it could be passed
208
208
when creating the ``factory `` instance.
209
209
210
210
If the module defining the product is known to only contain one smart contract, then checking the
211
- module reference is sufficient for identifying the code of the product smart contract. In this case ,
212
- however, both the factory and product contracts are defined in the same module, so it is necessary
213
- to also check the contract name. This is achieved as follows:
211
+ module reference is sufficient for identifying the code of the product smart contract. In the example smart contract ,
212
+ however, both the factory and product contracts are defined in the same module, so it is also necessary
213
+ to check the contract name. This is achieved as follows:
214
214
215
215
.. code-block :: Rust
216
216
217
217
let product_name =
218
218
host.contract_name(product_address).or(Err(FactoryError::NonExistentProduct))?;
219
219
ensure_eq!(product_name, PRODUCT_INIT_NAME, FactoryError::InvalidProduct);
220
220
221
- Now the contract is known to be an instance of ``product ``, the next step is to call
222
- ``initialize ``. In this example, ``initialize `` takes as a parameter that is the index assigned to
223
- it, which will be the current value of ``next_product `` in the state.
221
+ Now the contract is known to be an instance of ``product ``, the next step is to update the state of
222
+ the factory contract:
223
+
224
+ .. code-block :: Rust
225
+
226
+ let state = host.state_mut();
227
+ let next_product = state.next_product;
228
+ state.next_product = next_product + 1;
229
+ state.products.insert(next_product, product_address);
230
+
231
+ Finally, it remains to invoke ``initialize `` on the product.
232
+ In this example, ``initialize `` takes a parameter that is the index assigned to
233
+ it, which will be the old value of ``next_product `` in the state.
224
234
225
235
.. code-block :: Rust
226
236
227
- let next_product = host.state().next_product;
228
237
host.invoke_contract(
229
238
&product_address,
230
239
&next_product,
@@ -235,13 +244,6 @@ it, which will be the current value of ``next_product`` in the state.
235
244
236
245
Here, it is assumed that ``initialize `` will fail, for instance, if it is called on a product that
237
246
has previously been initialized.
238
- It only remains to update the factory contract's state:
239
-
240
- .. code-block :: Rust
241
-
242
- let state = host.state_mut();
243
- state.next_product = next_product + 1;
244
- state.products.insert(next_product, product_address);
245
247
246
248
The ``product `` contract
247
249
------------------------
@@ -295,15 +297,15 @@ initialized:
295
297
Since the construction and initialization of the product occur in two
296
298
separate transactions, it is possible that a third party might try to hijack
297
299
the process by inserting their own transaction to initialize the product.
298
- For instance, an adversary could invoke a different factory instance than indended by the user,
300
+ For instance, an adversary could invoke a different factory instance than intended by the user,
299
301
as illustrated in the following sequence diagram:
300
302
301
303
.. image :: images/factory-adversary.svg
302
304
:alt: sequence diagram showing how a third party might hijack a product
303
305
304
306
To prevent this possibility, the product checks in its ``initialize `` method that the invoker of the
305
- transaction (i.e. the account that originated the transaction as a whole) is the same account as
306
- created the product contract instance (i.e. the "owner"):
307
+ transaction (i.e., the account that originated the transaction as a whole) is the same account as
308
+ created the product contract instance (i.e., the "owner"):
307
309
308
310
.. code-block :: Rust
309
311
@@ -319,7 +321,7 @@ the invoker Adversary does not match the owner User), but success for User:
319
321
Typically, it is wrong to use the invoker of a transaction for
320
322
authorization, rather than the immediate caller. For instance, a user might
321
323
invoke some untrusted smart contract, and expect it is not authorized to
322
- transfer tokens she holds on another contract. If the token-holding contract
324
+ transfer tokens they hold on another contract. If the token-holding contract
323
325
used the invoker for authorization, then the untrusted contract could
324
326
transfer the tokens. In the case of the factory pattern, however, the
325
327
authorization is for a one-time use (initializing the product contract)
@@ -331,7 +333,7 @@ the invoker Adversary does not match the owner User), but success for User:
331
333
.. image :: images/factory-tricked.svg
332
334
:alt: sequence diagram showing how a hijacking attempt may succeed if the user is deceived into signing a bad transaction
333
335
334
- This is hopefully unlikely. Moreover, the effect of
336
+ Hopefully, this is unlikely. Moreover, the effect of
335
337
such a hijacking should typically be that the product cannot be used as the
336
338
user intended, but the user would still be able to create another product
337
339
and have the factory produce that correctly.
@@ -362,7 +364,7 @@ At this point, it just remains to initialize the state of the product:
362
364
This is because ``initialize `` checks that the immediate caller is a smart contract, and
363
365
records the contract address in the state. The fact that this prevents a user from directly
364
366
invoking ``initialize `` is incidental. The design intent is not to prevent the user from
365
- badly initializing products (which they could also do by invoking a "BadFactory" as previously
367
+ initializing products badly (which they could also do by invoking a "BadFactory" as previously
366
368
noted). The intent is that products that are produced by the factory are produced correctly.
367
369
368
370
@@ -379,7 +381,7 @@ product before `initialize` is called, then the consequences and risk of
379
381
hijacking are more sever. Thus, to adhere to the factory pattern, the
380
382
product contract must:
381
383
382
- 1. Always be constructed in an uninitialized state, with no balance, authority or any other state.
384
+ 1. Always be constructed in an uninitialized state, with no balance, authority, or any other state.
383
385
384
386
2. Only permit the ``initialize `` update operation while it is in the uninitialized state.
385
387
0 commit comments