forked from Vero-protocol/vero-core-contracts
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmigrate.rs
More file actions
238 lines (211 loc) · 7.69 KB
/
Copy pathmigrate.rs
File metadata and controls
238 lines (211 loc) · 7.69 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
//! Storage versioning and migration utilities with safety circuit breakers.
//!
//! The contract records a `StorageVersion` (u32) in instance storage so that
//! future schema upgrades can detect the current format and apply the
//! appropriate transformation steps.
//!
//! In order to prevent partial state corruption, this module implements a
//! "Commit-or-Rollback" pattern using an in-memory `MigrationCache` and performs
//! an atomic "Pre-Flight" validation check before committing state changes.
use crate::types::{ContractError, DataKey};
use soroban_sdk::{log, Address, Env, IntoVal, Map, TryFromVal, Val};
/// The current on-chain storage schema version.
/// Increment this constant whenever the storage layout changes.
pub const CURRENT_VERSION: u32 = 1;
/// A temporary in-memory cache to hold state changes during a migration dry-run.
pub struct MigrationCache<'a> {
env: &'a Env,
updates: Map<DataKey, Val>,
removals: soroban_sdk::Vec<DataKey>,
}
impl<'a> MigrationCache<'a> {
/// Create a new migration cache.
pub fn new(env: &'a Env) -> Self {
Self {
env,
updates: Map::new(env),
removals: soroban_sdk::Vec::new(env),
}
}
/// Read a value, checking the temporary cache first.
pub fn get<V>(&self, key: &DataKey) -> Option<V>
where
V: TryFromVal<Env, Val>,
{
if self.has_removal(key) {
return None;
}
if let Some(val) = self.updates.get(key.clone()) {
V::try_from_val(self.env, &val).ok()
} else {
self.env.storage().instance().get(key)
}
}
/// Write a value to the temporary cache.
pub fn set<V>(&mut self, key: &DataKey, value: &V)
where
V: IntoVal<Env, Val>,
{
self.remove_from_removals(key);
self.updates.set(key.clone(), value.into_val(self.env));
}
/// Remove a key from the temporary cache.
#[allow(dead_code)]
pub fn remove(&mut self, key: &DataKey) {
self.updates.remove(key.clone());
if !self.has_removal(key) {
self.removals.push_back(key.clone());
}
}
fn has_removal(&self, key: &DataKey) -> bool {
for r in self.removals.iter() {
if &r == key {
return true;
}
}
false
}
fn remove_from_removals(&mut self, key: &DataKey) {
let mut new_removals = soroban_sdk::Vec::new(self.env);
for r in self.removals.iter() {
if &r != key {
new_removals.push_back(r);
}
}
self.removals = new_removals;
}
/// Commit all cached changes to the persistent instance storage.
pub fn commit(&self) {
for key in self.removals.iter() {
self.env.storage().instance().remove(&key);
}
for (key, val) in self.updates.iter() {
self.env.storage().instance().set(&key, &val);
}
}
}
/// Returns the storage version currently recorded on-chain.
/// Returns 0 if no version has been set (i.e. a pre-versioning contract).
pub fn get_version(env: &Env) -> u32 {
env.storage()
.instance()
.get(&DataKey::StorageVersion)
.unwrap_or(0)
}
/// Writes the given version into instance storage.
pub fn set_version(env: &Env, version: u32) {
env.storage()
.instance()
.set(&DataKey::StorageVersion, &version);
}
/// Returns `true` if the on-chain storage schema is older than the
/// current binary's `CURRENT_VERSION`, meaning a migration is required.
#[allow(dead_code)]
pub fn needs_migration(env: &Env) -> bool {
get_version(env) < CURRENT_VERSION
}
/// Run pre-flight checks to validate all contract invariants in the cache.
pub fn validate_migration(env: &Env, cache: &MigrationCache) -> Result<(), ContractError> {
// 1. Verify StorageVersion is CURRENT_VERSION
let version: u32 = cache.get(&DataKey::StorageVersion).unwrap_or(0);
if version != CURRENT_VERSION {
log!(
env,
"Validation failed: version mismatch. expected: {}, got: {}",
CURRENT_VERSION,
version
);
return Err(ContractError::InvalidVersion);
}
// 2. Validate Admin address if present in storage or cache
if let Some(admin) = cache.get::<Address>(&DataKey::Admin) {
if let Err(e) = crate::validation::validate_admin_address(env, &admin) {
log!(env, "Validation failed: invalid admin address.", admin);
return Err(e);
}
}
// 3. Validate TokenAddress if present in storage or cache
if let Some(token) = cache.get::<Address>(&DataKey::TokenAddress) {
if let Err(e) = crate::validation::validate_external_address(env, &token) {
log!(env, "Validation failed: invalid token address.", token);
return Err(e);
}
}
// 4. Validate VaultAddress if present
if let Some(vault) = cache.get::<Address>(&DataKey::VaultAddress) {
if let Err(e) = crate::validation::validate_external_address(env, &vault) {
log!(env, "Validation failed: invalid vault address.", vault);
return Err(e);
}
}
// 5. Validate DripsAddress if present
if let Some(drips) = cache.get::<Address>(&DataKey::DripsAddress) {
if let Err(e) = crate::validation::validate_external_address(env, &drips) {
log!(env, "Validation failed: invalid drips address.", drips);
return Err(e);
}
}
// 6. Validate WeightThreshold if present
if let Some(threshold) = cache.get::<u64>(&DataKey::WeightThreshold) {
if let Err(e) = crate::validation::validate_weight_threshold(threshold) {
log!(
env,
"Validation failed: invalid weight threshold.",
threshold
);
return Err(e);
}
}
// 7. Validate FeeBps if present (must be <= 10000 bps)
if let Some(fee_bps) = cache.get::<u32>(&DataKey::FeeBps) {
if fee_bps > 10000 {
log!(env, "Validation failed: invalid fee bps.", fee_bps);
return Err(ContractError::InvalidConfig);
}
}
Ok(())
}
/// Applies any pending storage migrations to bring the on-chain state
/// up to `CURRENT_VERSION`. The migration is **idempotent**: if the
/// storage is already at the latest version, this function returns
/// immediately without side-effects.
pub fn migrate(env: &Env) -> Result<(), ContractError> {
let current = get_version(env);
if current >= CURRENT_VERSION {
return Ok(()); // already up to date
}
log!(
env,
"Starting atomic storage migration from version {} to {}",
current,
CURRENT_VERSION
);
// Initialize temporary cache
let mut cache = MigrationCache::new(env);
// ── v0 → v1 ──────────────────────────────────────────────────
// Record CURRENT_VERSION in cache.
cache.set(&DataKey::StorageVersion, &CURRENT_VERSION);
// Future migrations (v1 → v2, etc.) will append transformation steps here, e.g.:
// if current < 2 {
// // transform v1 keys to v2 format in cache
// }
// Run Pre-Flight check
match validate_migration(env, &cache) {
Ok(_) => {
log!(
env,
"Migration pre-flight checks passed. Committing changes to storage."
);
cache.commit();
Ok(())
}
Err(err) => {
log!(
env,
"Migration FAILED at pre-flight check! Aborting and rolling back. Error: {:?}",
err
);
Err(err)
}
}
}