Loading...
Loading...
Compare original and translation side by side
#[callback]#[callback]#[endpoint]
fn transfer_and_update(&self, recipient: ManagedAddress, amount: BigUint) {
// State change happens IMMEDIATELY
self.total_sent().update(|t| *t += &amount);
// Async call to another contract
self.tx()
.to(&recipient)
.egld(&amount)
.callback(self.callbacks().on_transfer())
.async_call_and_exit();
}
#[callback]
fn on_transfer(&self) {
// If transfer FAILED, total_sent is STILL updated!
// This is inconsistent state!
}#[endpoint]
fn transfer_and_update(&self, recipient: ManagedAddress, amount: BigUint) {
// 状态变更会立即执行
self.total_sent().update(|t| *t += &amount);
// 异步调用其他合约
self.tx()
.to(&recipient)
.egld(&amount)
.callback(self.callbacks().on_transfer())
.async_call_and_exit();
}
#[callback]
fn on_transfer(&self) {
// 如果转账失败,total_sent仍会被更新!
// 这会导致状态不一致!
}#[endpoint]
fn transfer_and_update(&self, recipient: ManagedAddress, amount: BigUint) {
// DON'T update state before async call
self.tx()
.to(&recipient)
.egld(&amount)
.callback(self.callbacks().on_transfer(amount.clone()))
.async_call_and_exit();
}
#[callback]
fn on_transfer(&self, amount: BigUint, #[call_result] result: ManagedAsyncCallResult<()>) {
match result {
ManagedAsyncCallResult::Ok(_) => {
// Only update state on SUCCESS
self.total_sent().update(|t| *t += &amount);
},
ManagedAsyncCallResult::Err(_) => {
// Handle failure explicitly
// Funds return to contract automatically
}
}
}#[endpoint]
fn transfer_and_update(&self, recipient: ManagedAddress, amount: BigUint) {
// 异步调用前不要更新状态
self.tx()
.to(&recipient)
.egld(&amount)
.callback(self.callbacks().on_transfer(amount.clone()))
.async_call_and_exit();
}
#[callback]
fn on_transfer(&self, amount: BigUint, #[call_result] result: ManagedAsyncCallResult<()>) {
match result {
ManagedAsyncCallResult::Ok(_) => {
// 仅在调用成功时更新状态
self.total_sent().update(|t| *t += &amount);
},
ManagedAsyncCallResult::Err(_) => {
// 显式处理失败情况
// 资金会自动退回合约
}
}
}1. Sender shard processes transaction (state changed)
2. Receiver shard runs out of gas
3. Receiver execution fails
4. Sender state changes PERSIST
5. Callback triggered with error1. 发送方分片处理交易(状态已变更)
2. 接收方分片燃气耗尽
3. 接收方执行失败
4. 发送方的状态变更仍会保留
5. 触发携带错误信息的回调// DON'T: Skip gas reservation for callbacks — OOG in callback loses state
self.tx().to(&other).typed(Proxy).call()
.callback(self.callbacks().on_result())
.async_call_and_exit(); // No gas reserved for callback!// 错误做法:不为回调预留燃气 — 回调中燃气耗尽会导致状态丢失
self.tx().to(&other).typed(Proxy).call()
.callback(self.callbacks().on_result())
.async_call_and_exit(); // 未为回调预留燃气!// DO: Always reserve explicit gas for callbacks
self.tx().to(&other).typed(Proxy).call()
.gas(50_000_000)
.callback(self.callbacks().on_result())
.gas_for_callback(10_000_000) // Ensures callback can execute
.async_call_and_exit();// 正确做法:始终为回调显式预留燃气
self.tx().to(&other).typed(Proxy).call()
.gas(50_000_000)
.callback(self.callbacks().on_result())
.gas_for_callback(10_000_000) // 确保回调可以执行
.async_call_and_exit();// Always reserve enough gas for callbacks
const CALLBACK_GAS: u64 = 10_000_000;
#[endpoint]
fn safe_cross_shard(&self) {
self.tx()
.to(&other_contract)
.typed(proxy::Proxy)
.function()
.gas(50_000_000)
.callback(self.callbacks().handle_result())
.gas_for_callback(CALLBACK_GAS)
.async_call_and_exit();
}// 始终为回调预留足够的燃气
const CALLBACK_GAS: u64 = 10_000_000;
#[endpoint]
fn safe_cross_shard(&self) {
self.tx()
.to(&other_contract)
.typed(proxy::Proxy)
.function()
.gas(50_000_000)
.callback(self.callbacks().handle_result())
.gas_for_callback(CALLBACK_GAS)
.async_call_and_exit();
}VecMapperVecVecMapperVec// VecMapper: Each element is a separate storage slot
// Accessing element = 1 storage read
// Iterating N elements = N storage reads
#[storage_mapper("users")]
fn users(&self) -> VecMapper<ManagedAddress>;
// If you load into a Vec, you load EVERYTHING into WASM memory
fn bad_function(&self) {
let all_users: Vec<ManagedAddress> = self.users().iter().collect();
// With 10,000 users = 10,000 storage reads + massive memory allocation
// WILL run out of gas
}// VecMapper:每个元素对应独立的存储槽
// 访问单个元素 = 1次存储读取操作
// 遍历N个元素 = N次存储读取操作
#[storage_mapper("users")]
fn users(&self) -> VecMapper<ManagedAddress>;
// 如果加载到Vec中,会将所有数据加载到WASM内存
fn bad_function(&self) {
let all_users: Vec<ManagedAddress> = self.users().iter().collect();
// 若有10000个用户,会产生10000次存储读取 + 大量内存分配
// 必然会燃气耗尽
}// Paginate operations
fn process_users_paginated(&self, start: usize, count: usize) {
let len = self.users().len();
let end = (start + count).min(len);
for i in start..end {
let user = self.users().get(i + 1); // VecMapper is 1-indexed!
self.process_user(&user);
}
}
// Or use appropriate mapper for the use case
// SetMapper for O(1) contains checks
// UnorderedSetMapper for efficient removal// 分页处理操作
fn process_users_paginated(&self, start: usize, count: usize) {
let len = self.users().len();
let end = (start + count).min(len);
for i in start..end {
let user = self.users().get(i + 1); // VecMapper是从1开始索引的!
self.process_user(&user);
}
}
// 或根据使用场景选择合适的映射器
// 如需O(1)时间复杂度的存在性检查,使用SetMapper
// 如需高效删除操作,使用UnorderedSetMapper// WRONG: Assumes 18 decimals
fn convert_to_usd(&self, token_amount: BigUint) -> BigUint {
let price = self.price().get(); // Price in 10^18
&token_amount * &price / BigUint::from(10u64.pow(18)) // Assumes 18 decimals!
}// 错误做法:假设代币是18位小数
fn convert_to_usd(&self, token_amount: BigUint) -> BigUint {
let price = self.price().get(); // 价格单位为10^18
&token_amount * &price / BigUint::from(10u64.pow(18)) // 硬编码18位小数!
}fn convert_to_usd(&self, token_amount: BigUint, token_decimals: u8) -> BigUint {
let price = self.price().get();
let decimal_factor = BigUint::from(10u64).pow(token_decimals as u32);
&token_amount * &price / &decimal_factor
}
// Or require specific decimals
fn require_standard_decimals(&self, token_id: &TokenIdentifier) {
let properties = self.blockchain().get_esdt_token_data(
&self.blockchain().get_sc_address(),
token_id,
0
);
require!(properties.decimals == 18, "Token must have 18 decimals");
}fn convert_to_usd(&self, token_amount: BigUint, token_decimals: u8) -> BigUint {
let price = self.price().get();
let decimal_factor = BigUint::from(10u64).pow(token_decimals as u32);
&token_amount * &price / &decimal_factor
}
// 或要求代币必须使用特定小数位数
fn require_standard_decimals(&self, token_id: &TokenIdentifier) {
let properties = self.blockchain().get_esdt_token_data(
&self.blockchain().get_sc_address(),
token_id,
0
);
require!(properties.decimals == 18, "代币必须为18位小数");
}#[init]#[upgrade]#[init]#[upgrade]// V1 contract
#[init]
fn init(&self) {
self.version().set(1);
}
// V2 contract - added new storage
#[init]
fn init(&self) {
self.version().set(2);
self.new_feature_enabled().set(true); // NEVER RUNS ON UPGRADE!
}
// After upgrade: version is still 1, new_feature_enabled is empty!// V1合约
#[init]
fn init(&self) {
self.version().set(1);
}
// V2合约 - 新增了存储变量
#[init]
fn init(&self) {
self.version().set(2);
self.new_feature_enabled().set(true); // 升级时永远不会执行!
}
// 升级后:version仍为1,new_feature_enabled的值为空!#[upgrade]
fn upgrade(&self) {
// Initialize new storage here
self.version().set(2);
self.new_feature_enabled().set(true);
// Migrate existing data if needed
self.migrate_storage();
}#[upgrade]
fn upgrade(&self) {
// 在此处初始化新增的存储变量
self.version().set(2);
self.new_feature_enabled().set(true);
// 如有需要,迁移现有数据
self.migrate_storage();
}// V1
struct UserData {
balance: BigUint, // Encoded at position 0
timestamp: u64, // Encoded at position 1
}
// V2 - BREAKS EXISTING DATA
struct UserData {
timestamp: u64, // Now at position 0 - reads old balance bytes!
balance: BigUint, // Now at position 1 - reads old timestamp bytes!
new_field: bool, // This is fine (appended)
}// V1
struct UserData {
balance: BigUint, // 编码在位置0
timestamp: u64, // 编码在位置1
}
// V2 - 会破坏现有数据
struct UserData {
timestamp: u64, // 现在在位置0 - 会读取旧的balance字节!
balance: BigUint, // 现在在位置1 - 会读取旧的timestamp字节!
new_field: bool, // 这个没问题(追加字段)
}get_block_timestamp_millis()get_block_timestamp_seconds()#[view]get_block_timestamp_millis()TimestampMillisTimestampSeconds#[view]get_block_timestamp_millis()get_block_timestamp_seconds()get_block_timestamp_millis()TimestampMillisTimestampSeconds// Problem - using seconds loses precision with Supernova's 0.6s rounds
#[view(isExpired)]
fn is_expired(&self) -> bool {
let deadline = self.deadline().get(); // TimestampMillis
let current_time = self.blockchain().get_block_timestamp_millis();
// Off-chain simulation may return 0 or stale value!
current_time > deadline
}// 问题 - 使用秒级时间戳会在Supernova的0.6秒出块机制下丢失精度
#[view(isExpired)]
fn is_expired(&self) -> bool {
let deadline = self.deadline().get(); // TimestampMillis类型
let current_time = self.blockchain().get_block_timestamp_millis();
// 链下模拟可能返回0或过期的值!
current_time > deadline
}// Option 1: Don't rely on block info in views
#[view(getDeadline)]
fn get_deadline(&self) -> TimestampMillis {
self.deadline().get()
// Let client compare with their known current time
}
// Option 2: Accept timestamp as parameter for queries
#[view(isExpiredAt)]
fn is_expired_at(&self, check_time: TimestampMillis) -> bool {
let deadline = self.deadline().get();
check_time > deadline
}// 方案1:不在视图函数中依赖区块信息
#[view(getDeadline)]
fn get_deadline(&self) -> TimestampMillis {
self.deadline().get()
// 由客户端自行与本地当前时间比较
}
// 方案2:将时间戳作为参数传入查询
#[view(isExpiredAt)]
fn is_expired_at(&self, check_time: TimestampMillis) -> bool {
let deadline = self.deadline().get();
check_time > deadline
}VecMapperVecVecMapperVecfn get_first_user(&self) -> ManagedAddress {
self.users().get(0) // PANIC! Index 0 doesn't exist
}fn get_first_user(&self) -> ManagedAddress {
self.users().get(0) // 会 panic!索引0不存在
}fn get_first_user(&self) -> ManagedAddress {
require!(!self.users().is_empty(), "No users");
self.users().get(1) // First element is at index 1
}
fn iterate_users(&self) {
for i in 1..=self.users().len() { // 1 to len, inclusive
let user = self.users().get(i);
// process user
}
}fn get_first_user(&self) -> ManagedAddress {
require!(!self.users().is_empty(), "无用户数据");
self.users().get(1) // 第一个元素在索引1的位置
}
fn iterate_users(&self) {
for i in 1..=self.users().len() { // 从1到长度值,包含两端
let user = self.users().get(i);
// 处理用户数据
}
}PaymentTokenId.egld().single_esdt()PaymentTokenId.egld().single_esdt()// This used to be impossible, now supported via unified Payment API// 过去无法实现,现在通过统一Payment API支持// Use unified Payment with TokenId for mixed transfers
let mut payments = ManagedVec::new();
if let Some(egld_nz) = egld_amount.into_non_zero() {
payments.push(Payment::new(TokenId::from("EGLD-000000"), 0, egld_nz));
}
if let Some(esdt_nz) = esdt_amount.into_non_zero() {
payments.push(Payment::new(TokenId::from(token_id), 0, esdt_nz));
}
self.tx().to(&recipient).payment(&payments).transfer();// 使用统一的Payment搭配TokenId实现混合转账
let mut payments = ManagedVec::new();
if let Some(egld_nz) = egld_amount.into_non_zero() {
payments.push(Payment::new(TokenId::from("EGLD-000000"), 0, egld_nz));
}
if let Some(esdt_nz) = esdt_amount.into_non_zero() {
payments.push(Payment::new(TokenId::from(token_id), 0, esdt_nz));
}
self.tx().to(&recipient).payment(&payments).transfer();MapMapper4*N + 1MapMapper4*N + 1// DON'T: Use MapMapper for per-user data when you don't need iteration
// For 1000 users, this creates 4001 storage entries!
#[storage_mapper("balances")]
fn balances(&self) -> MapMapper<ManagedAddress, BigUint>;// 错误做法:当不需要遍历功能时,不要用MapMapper存储用户数据
// 1000个用户会创建4001个存储条目!
#[storage_mapper("balances")]
fn balances(&self) -> MapMapper<ManagedAddress, BigUint>;// DO: Use SingleValueMapper with address key — 1 entry per user
#[storage_mapper("balance")]
fn balance(&self, user: &ManagedAddress) -> SingleValueMapper<BigUint>;
// Only use MapMapper when you MUST iterate over all entries// 正确做法:使用带地址键的SingleValueMapper — 每个用户仅对应1个存储条目
#[storage_mapper("balance")]
fn balance(&self, user: &ManagedAddress) -> SingleValueMapper<BigUint>;
// 仅当必须遍历所有条目时才使用MapMapperrequire!sc_panic!require!sc_panic!// Each unique string increases WASM size
require!(condition1, "Error message one");
require!(condition2, "Error message two");
require!(condition3, "Error message three");// 每个唯一的字符串都会增加WASM体积
require!(condition1, "错误信息1");
require!(condition2, "错误信息2");
require!(condition3, "错误信息3");// Use static error constants
const ERR_INVALID_AMOUNT: &str = "Invalid amount";
const ERR_UNAUTHORIZED: &str = "Unauthorized";
require!(amount > 0, ERR_INVALID_AMOUNT);
require!(caller == owner, ERR_UNAUTHORIZED);
// Reuse same constant for same error type
require!(amount1 > 0, ERR_INVALID_AMOUNT);
require!(amount2 > 0, ERR_INVALID_AMOUNT);// 使用静态错误常量
const ERR_INVALID_AMOUNT: &str = "金额无效";
const ERR_UNAUTHORIZED: &str = "无权限";
require!(amount > 0, ERR_INVALID_AMOUNT);
require!(caller == owner, ERR_UNAUTHORIZED);
// 同一错误类型复用相同的常量
require!(amount1 > 0, ERR_INVALID_AMOUNT);
require!(amount2 > 0, ERR_INVALID_AMOUNT);Payment.amountNonZeroBigUintBigUintBigUintPayment.amountNonZeroBigUintBigUintBigUint// WRONG - won't compile, Payment expects NonZeroBigUint
let payment = Payment::new(token_id, 0, amount); // amount is BigUint
// WRONG - panics at runtime if amount is zero
let nz = NonZeroBigUint::new_or_panic(amount);// 错误 - 编译不通过,Payment期望NonZeroBigUint类型
let payment = Payment::new(token_id, 0, amount); // amount是BigUint类型
// 错误 - 如果amount为0,运行时会panic
let nz = NonZeroBigUint::new_or_panic(amount);// Option-based (safe)
if let Some(amount_nz) = amount.into_non_zero() {
let payment = Payment::new(token_id, 0, amount_nz);
self.tx().to(&to).payment(payment).transfer();
}
// When reading from call_value, amount is already NonZeroBigUint
let payment = self.call_value().single();
// payment.amount is NonZeroBigUint — guaranteed non-zero
// Use .as_big_uint() to get a &BigUint reference for arithmetic
self.balance(&caller).update(|b| *b += payment.amount.as_big_uint());// 基于Option的安全处理方式
if let Some(amount_nz) = amount.into_non_zero() {
let payment = Payment::new(token_id, 0, amount_nz);
self.tx().to(&to).payment(payment).transfer();
}
// 从call_value读取时,金额已经是NonZeroBigUint类型
let payment = self.call_value().single();
// payment.amount是NonZeroBigUint类型 — 保证非零
// 使用.as_big_uint()获取&BigUint引用以进行算术运算
self.balance(&caller).update(|b| *b += payment.amount.as_big_uint());require!(amount > 0, ...)BigUintrequire!(amount > 0, ...)BigUint#[endpoint]
fn multi_swap(&self, dex: ManagedAddress) {
// First swap
let bt1 = self.tx().to(&dex).typed(DexProxy)
.swap_a()
.returns(ReturnsBackTransfers) // No reset!
.sync_call();
// Second swap
let bt2 = self.tx().to(&dex).typed(DexProxy)
.swap_b()
.returns(ReturnsBackTransfers) // No reset!
.sync_call();
// BUG: bt2 contains payments from BOTH swap_a AND swap_b
let total = bt2.into_payment_vec(); // Wrong amount!
}#[endpoint]
fn multi_swap(&self, dex: ManagedAddress) {
// 第一次交换
let bt1 = self.tx().to(&dex).typed(DexProxy)
.swap_a()
.returns(ReturnsBackTransfers) // 未重置!
.sync_call();
// 第二次交换
let bt2 = self.tx().to(&dex).typed(DexProxy)
.swap_b()
.returns(ReturnsBackTransfers) // 未重置!
.sync_call();
// 错误:bt2包含swap_a和swap_b两次调用的回传资金
let total = bt2.into_payment_vec(); // 金额错误!
}#[endpoint]
fn multi_swap(&self, dex: ManagedAddress) {
let bt1 = self.tx().to(&dex).typed(DexProxy)
.swap_a()
.returns(ReturnsBackTransfersReset) // Resets before reading
.sync_call();
let bt2 = self.tx().to(&dex).typed(DexProxy)
.swap_b()
.returns(ReturnsBackTransfersReset) // Resets before reading
.sync_call();
// bt1 and bt2 each contain only their own call's payments
}#[endpoint]
fn multi_swap(&self, dex: ManagedAddress) {
let bt1 = self.tx().to(&dex).typed(DexProxy)
.swap_a()
.returns(ReturnsBackTransfersReset) // 读取前重置
.sync_call();
let bt2 = self.tx().to(&dex).typed(DexProxy)
.swap_b()
.returns(ReturnsBackTransfersReset) // 读取前重置
.sync_call();
// bt1和bt2分别只包含对应调用的回传资金
}ReturnsBackTransfersResetReturnsBackTransfersResetblockchain().reset_back_transfers()ReturnsBackTransfersResetReturnsBackTransfersblockchain().reset_back_transfers()#[endpoint]
fn delegate_to_provider(&self, provider: ManagedAddress, amount: BigUint) {
self.pending_amount().update(|p| *p += &amount);
self.tx().to(&provider)
.typed(ProviderProxy).delegate()
.egld(&amount)
.callback(self.callbacks().on_delegate())
.async_call_and_exit();
// If callback never fires, pending_amount is stuck forever
}#[endpoint]
fn delegate_to_provider(&self, provider: ManagedAddress, amount: BigUint) {
self.pending_amount().update(|p| *p += &amount);
self.tx().to(&provider)
.typed(ProviderProxy).delegate()
.egld(&amount)
.callback(self.callbacks().on_delegate())
.async_call_and_exit();
// 如果回调永远不触发,pending_amount会一直处于待处理状态
}#[endpoint]
fn delegate_to_provider(&self, provider: ManagedAddress, amount: BigUint) {
// Track the pending operation with a unique ID
let op_id = self.next_op_id().update(|id| { *id += 1; *id });
self.pending_operations(op_id).set(PendingOp {
provider: provider.clone(),
amount: amount.clone(),
timestamp: self.blockchain().get_block_timestamp_millis(),
});
self.tx().to(&provider)
.typed(ProviderProxy).delegate()
.egld(&amount)
.callback(self.callbacks().on_delegate(op_id))
.async_call_and_exit();
}
#[callback]
fn on_delegate(&self, op_id: u64, #[call_result] result: ManagedAsyncCallResult<()>) {
self.pending_operations(op_id).clear(); // Always clear tracking
match result {
ManagedAsyncCallResult::Ok(_) => { /* success */ }
ManagedAsyncCallResult::Err(_) => { /* handle failure, refund etc */ }
}
}
// Admin recovery for stuck operations
#[endpoint(recoverPending)]
fn recover_pending(&self, op_id: u64) {
require!(self.blockchain().get_caller() == self.blockchain().get_owner_address(), "Not owner");
let op = self.pending_operations(op_id).get();
let now = self.blockchain().get_block_timestamp_millis();
require!(now - op.timestamp > RECOVERY_TIMEOUT_MS, "Too early to recover");
self.pending_operations(op_id).clear();
// Refund or retry logic
}#[endpoint]
fn delegate_to_provider(&self, provider: ManagedAddress, amount: BigUint) {
// 用唯一ID追踪待处理操作
let op_id = self.next_op_id().update(|id| { *id += 1; *id });
self.pending_operations(op_id).set(PendingOp {
provider: provider.clone(),
amount: amount.clone(),
timestamp: self.blockchain().get_block_timestamp_millis(),
});
self.tx().to(&provider)
.typed(ProviderProxy).delegate()
.egld(&amount)
.callback(self.callbacks().on_delegate(op_id))
.async_call_and_exit();
}
#[callback]
fn on_delegate(&self, op_id: u64, #[call_result] result: ManagedAsyncCallResult<()>) {
self.pending_operations(op_id).clear(); // 无论结果如何,都清除追踪记录
match result {
ManagedAsyncCallResult::Ok(_) => { /* 处理成功逻辑 */ }
ManagedAsyncCallResult::Err(_) => { /* 处理失败逻辑,比如退款等 */ }
}
}
// 管理员恢复卡住的操作
#[endpoint(recoverPending)]
fn recover_pending(&self, op_id: u64) {
require!(self.blockchain().get_caller() == self.blockchain().get_owner_address(), "非合约所有者");
let op = self.pending_operations(op_id).get();
let now = self.blockchain().get_block_timestamp_millis();
require!(now - op.timestamp > RECOVERY_TIMEOUT_MS, "恢复时机未到");
self.pending_operations(op_id).clear();
// 退款或重试逻辑
}storage_mapper_from_address#[storage_mapper_from_address("key")]#[storage_mapper_from_address("key")]// Your contract reads the "reserve" key from a DEX pair
#[storage_mapper_from_address("reserve")]
fn pair_reserve(&self, addr: ManagedAddress, token: &TokenIdentifier)
-> SingleValueMapper<BigUint, ManagedAddress>;
// DEX upgrades and renames "reserve" to "token_reserve"
// Your reads now return 0 — silently incorrect data!// 你的合约读取DEX交易对的"reserve"键
#[storage_mapper_from_address("reserve")]
fn pair_reserve(&self, addr: ManagedAddress, token: &TokenIdentifier)
-> SingleValueMapper<BigUint, ManagedAddress>;
// DEX升级后将"reserve"重命名为"token_reserve"
// 你的读取操作现在会返回0 — 数据错误但无任何提示!fn get_pair_reserve_safe(&self, pair: &ManagedAddress, token: &TokenIdentifier) -> BigUint {
let reserve = self.pair_reserve(pair.clone(), token).get();
// Sanity check — active pairs should never have zero reserves
if reserve == 0u64 {
// Fallback: use proxy call or revert
sc_panic!("Unexpected zero reserve — target contract may have changed storage layout");
}
reserve
}fn get_pair_reserve_safe(&self, pair: &ManagedAddress, token: &TokenIdentifier) -> BigUint {
let reserve = self.pair_reserve(pair.clone(), token).get();
// 合理性检查 — 活跃交易对的储备金绝不可能为0
if reserve == 0u64 {
// 回退:使用代理调用或抛出错误
sc_panic!("储备金为0,异常 — 目标合约可能已变更存储布局");
}
reserve
}async_call_and_exit()drop()async_call_and_exit()drop()fn bad_pattern(&self) {
let mut cache = StorageCache::new(self);
cache.balance += &deposit_amount;
// async_call_and_exit() terminates execution — drop() NEVER runs!
self.tx().to(&other).typed(Proxy).call()
.callback(self.callbacks().on_result())
.async_call_and_exit();
// cache.drop() never fires — balance change is LOST
}fn bad_pattern(&self) {
let mut cache = StorageCache::new(self);
cache.balance += &deposit_amount;
// async_call_and_exit()会立即终止执行 — drop()永远不会运行!
self.tx().to(&other).typed(Proxy).call()
.callback(self.callbacks().on_result())
.async_call_and_exit();
// cache.drop()未执行 — 余额变更丢失
}fn good_pattern(&self) {
{
let mut cache = StorageCache::new(self);
cache.balance += &deposit_amount;
} // cache.drop() fires here — writes committed
self.tx().to(&other).typed(Proxy).call()
.callback(self.callbacks().on_result())
.async_call_and_exit();
}fn good_pattern(&self) {
{
let mut cache = StorageCache::new(self);
cache.balance += &deposit_amount;
} // cache.drop()在此执行 — 写入操作提交
self.tx().to(&other).typed(Proxy).call()
.callback(self.callbacks().on_result())
.async_call_and_exit();
}ManagedDecimalManagedDecimal// Each deposit loses a fraction of a token due to truncation
// Attacker makes 1000 tiny deposits, each time extracting the rounding difference
let shares = (amount * total_shares) / total_supply; // Truncates!// 每次存款都会丢失一小部分代币(截断导致)
// 攻击者发起1000次小额存款,每次提取舍入差额
let shares = (amount * total_shares) / total_supply; // 会截断!multiversx-defi-mathmultiversx-defi-math| Issue | Wrong | Right |
|---|---|---|
| VecMapper index | | |
| Callback state | Update before async | Update in callback on success |
| Upgrade init | Rely on | Use |
| Decimals | Hardcode | Fetch from token properties |
| MapMapper | Use for per-user data | Use SingleValueMapper with key |
| Block info in view | Direct use | Pass as parameter |
| EGLD + ESDT | Old: same tx impossible | Use unified |
| NonZeroBigUint | | |
| Struct fields | Reorder | Only append |
| BackTransfers | | |
| Pending callbacks | Fire-and-forget async | Track with op ID + recovery endpoint |
| Cross-contract storage keys | Assume keys never change | Sanity checks + version pinning |
| Cache + async | Drop cache before async call | Manual commit in callback only |
| Financial rounding | Default truncation | Half-up rounding (mul_half_up/div_half_up) |
| 问题 | 错误做法 | 正确做法 |
|---|---|---|
| VecMapper索引 | | |
| 回调状态处理 | 异步调用前更新状态 | 仅在回调成功时更新状态 |
| 升级初始化 | 依赖 | 使用 |
| 小数位数 | 硬编码 | 从代币属性中获取 |
| MapMapper使用 | 用于存储用户数据 | 使用带键的SingleValueMapper |
| 视图函数中的区块信息 | 直接使用 | 作为参数传入 |
| EGLD+ESDT转账 | 旧版:无法在同一笔交易中实现 | 使用带 |
| NonZeroBigUint使用 | | 先调用 |
| 结构体字段 | 重新排序 | 仅追加字段 |
| 回传资金处理 | 多次调用使用 | 使用 |
| 待处理回调 | 异步调用后不管不顾 | 用操作ID追踪 + 恢复端点 |
| 跨合约存储键 | 假设键永远不变 | 添加合理性检查 + 版本锁定 |
| 缓存+异步 | 异步调用前依赖缓存自动销毁 | 手动提交或仅在回调中提交 |
| 金融计算舍入 | 默认截断 | 使用四舍五入(mul_half_up/div_half_up) |