Develop a system facilitating easy updating or versioning of smart contracts. This system should serve as a template, wrapping the main logic of a smart contract to enable seamless handling of different versions. Note that in this context, "wrapping" refers to a smart contract encapsulating another, adding versioning functionality, and is not to be confused with contract wrappers in the Blueprint SDK.
Understanding the motivation behind this task aids in its comprehension. Smart contracts are inherently static; once deployed, their code is immutable unless they are designed for code modification. Replacing code might suffice in simple scenarios, but often it's inadequate, especially when:
- Smart contracts receive messages from other contracts. It's crucial to avoid processing old messages intended for a previous version with a new version, as message formats and data interpretation may differ.
- The structure of data stored in the smart contract's storage might evolve.
Your goal is to create a "universal smart contract versioning template" addressing these issues, allowing any smart contract to become upgradeable as described. While ideally, users wouldn't need to worry about versioning, some simple modifications in their smart contracts are necessary to support it:
- Use
process_message
instead ofrecv_internal
for incoming message processing. - Retrieve contract data from the
storage
parameter inprocess_message
, not throughget_data()
. - Update contract data by returning a new data cell from
process_message
, rather than usingset_data()
. - Replace
get_data()
withget_storage()
in get methods.
External messages are excluded for simplicity.
Additionally, when the storage format changes, your wrapper must call a cell migrate_one(cell old_storage)
method for storage format conversion (by first calling set_c3(migration_code)
to access it). This allows the wrapper to automatically migrate the storage cell to the new format without altering the main contract logic.
This task, while resembling a real-life scenario, has been simplified for clarity. Notably, it lacks security measures—anyone could update the code to a new version. Real-world applications would require additional features and modifications for blockchain deployment.
Implement a wrapper (template) with four methods:
recv_internal
- Handles internal messages from users.
- Checks if an update is necessary.
- if an update is needed - it migrates the storage and updates the contract code; if the update is needed but update code isn't attached, it throws an error
200
. - Executes
process_message
and updates storage.
cell get_storage()
- Returns the main smart contract's unwrapped storage.cell wrap_storage(int version_id, cell storage)
- Wraps the main contract's storage with versioning data.int version() method_id
- Returns the smart contract's current version.
Refer to the 3.fc
template for more details, including the required structure.
Important to note that code update and all processing of incoming message should be done in one transaction.
To deploy a smart contract you have to compose its Stateinit, which includes the contract's code and data. Getting the code simply means compilation, but the data varies in this task based on how you store versioning data (e.g., version_id
). For simplicity and generalization, let's assume that Stateinit's data should contain only the main smart contract data, as if it were deployed without versioning functionality. Your template must determine whether the contract is being invoked for the first time. If it is, the template should wrap the data in storage for future interactions.
Let's agree that the first call to the smart contract will occur with expected_version = 0
, followed by an empty migrations
dictionary. During this first call, your template must set the version to 1 for future interactions and finish the execution.
- The first 32 bits indicate the expected version of the contract.
- Next is a bit indicating whether update code is attached. If the bit is set to
1
, it is followed by the update code in a reference. - A dictionary containing migrations:
from_version => MigrationPayload
. payload
, which is the pure payload passed toprocess_message
. This payload is what the smart contract would receive if it were to operate without versioning functionality.
_ new_version:uint32 migration_code:(Maybe ^Cell) = MigrationPayload;
_ expected_version:uint32 new_code:(Maybe ^Cell) migrations:(HashmapE 32 MigrationPayload) payload:^Cell = InternalMsgBody;
Imagine you have a smart contract at version 1, and you want to update it to version 4. In between these versions, there might be changes in how the contract stores its data (the "storage format"). To smoothly move from one version to another without losing or corrupting data, you need to update the storage format step by step by calling migrate_one
function on storage data. This process is called "migration".
Think of migration as a series of small steps to get from your current version to the desired version. For example, if your contract has versions 1, 2, 3, and 4, you can't jump directly from 1 to 4 without first considering what changes occurred in versions 2 and 3.
Let's break down a few scenarios:
-
Simple Migration (1 -> 4):
- If there are no storage format changes from versions 1 to 4, you can directly move from 1 to 4.
- Example:
1 -> 4
with no storage migration needed.
-
Step-by-Step Migration (1 -> 2 -> 3 -> 4):
- If some intermediate versions have changes in storage format, you would need to update each of these in sequence.
- Example:
1 -> 2
(no change),2 -> 3
(migrate storage),3 -> 4
(migrate storage).
When handling incoming messages in your wrapper (the extra layer you're adding for versioning), you will have a "migration dictionary". This is a list of which versions need storage migration. For example:
3 -> 5
: No storage migration5 -> 6
: No storage migration6 -> 10
: Migrate storage8 -> 9
: No storage migration9 -> 10
: No storage migration10 -> 11
: Migrate storage
So, if you are migrating from version 3 to 11, you look at this dictionary and see which steps involve storage migration. In this case, you'll need to perform the migration for 6 -> 10
and 10 -> 11
.
Your wrapper should also include checks for missing links in the migration chain. For instance, if there is an attempt to upgrade from version 1 to 4, but the migration dictionary only contains migrations for 1 -> 2
and 3 -> 4
, it should identify this gap and throw an error 400
. This is because it cannot manage the storage format changes that occur in the missing version.
Additionaly, it's important to ensure that both the current and the expected versions are included in the migrations dictionary provided in an incoming message. If either of these versions is not present in the dictionary, the wrapper should throw an error 400
.
The main smart contract operates with "clean storage", unaware of versioning. Your template should manage the "wrapped storage", containing both clean storage and versioning data. get_storage
should return the clean storage passed to process_message
.
The important thing is that the way this "clean storage" behaves must be exactly the same as the regular storage would behave in the main smart contract without versioning functionality.
Follow these guidelines in your solution:
- Don't alter the arguments or names of
process_message
andmigrate_one
. - Ensure
get_storage
returns the storage used inprocess_message
. - The code separation indicated by
<<<<<
and>>>>>
in the template is crucial. It is utilized by the testing system to evaluate your template with various smart contracts. Your final code must include these splitting comments, as shown in the provided example.
The whole secret lies in confusing the enemy, so that he cannot fathom our real intent.
― Sun Tzu, The Art of War