Skip to content

Commit 1b5f99c

Browse files
authored
Add balance tree and output message effects to CEI analysis (FuelLabs#3648)
1 parent 2faff70 commit 1b5f99c

File tree

43 files changed

+294
-68
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+294
-68
lines changed

docs/book/src/blockchain-development/calling_contracts.md

+7-1
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ warning
141141
29 | |
142142
30 | | // Storage update _after_ external call
143143
31 | | storage.balances.insert(sender, 0);
144-
| |__________________________________________- Storage modification after external contract interaction in function or method "withdraw". Consider making all storage writes before calling another contract
144+
| |__________________________________________- Storage write after external contract interaction in function or method "withdraw". Consider making all storage writes before calling another contract
145145
32 | }
146146
33 | }
147147
|
@@ -150,6 +150,12 @@ ____
150150
151151
In case there is a storage read after an interaction, the CEI analyzer will issue a similar warning.
152152
153+
In addition to storage reads and writes after an interaction, the CEI analyzer reports analogous warnings about:
154+
155+
- balance tree updates, i.e. balance tree reads with subsequent writes, which may be produced by the `tr` and `tro` asm instructions or library functions using them under the hood;
156+
- balance trees reads with `bal` instruction;
157+
- changes to the output messages that can be produced by the `__smo` intrinsic function or the `smo` asm instruction.
158+
153159
## Differences from the EVM
154160
155161
While the Fuel contract calling paradigm is similar to the EVM's (using an ABI, forwarding gas and data), it differs in _two_ key ways:

sway-core/src/semantic_analysis/cei_pattern_analysis.rs

+62-37
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
// for more detail on vulnerabilities in case of storage modification after interaction
66
// and this [blog post](https://chainsecurity.com/curve-lp-oracle-manipulation-post-mortem)
77
// for more information on storage reads after interaction.
8+
// We also treat the balance tree reads and writes separately,
9+
// as well as modifying output messages.
810

911
use crate::{
1012
declaration_engine::{DeclarationEngine, DeclarationId},
@@ -15,6 +17,7 @@ use crate::{
1517
Engines,
1618
};
1719
use std::collections::HashSet;
20+
use std::fmt;
1821
use sway_error::warning::{CompileWarning, Warning};
1922
use sway_types::{Ident, Span, Spanned};
2023

@@ -23,6 +26,38 @@ enum Effect {
2326
Interaction, // interaction with external contracts
2427
StorageWrite, // storage modification
2528
StorageRead, // storage read
29+
// Note: there are no operations that only write to the balance tree
30+
BalanceTreeRead, // balance tree read operation
31+
BalanceTreeReadWrite, // balance tree read and write operation
32+
OutputMessage, // operation creates a new `Output::Message`
33+
}
34+
35+
impl fmt::Display for Effect {
36+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
37+
use Effect::*;
38+
match self {
39+
Interaction => write!(f, "Interaction"),
40+
StorageWrite => write!(f, "Storage write"),
41+
StorageRead => write!(f, "Storage read"),
42+
BalanceTreeRead => write!(f, "Balance tree read"),
43+
BalanceTreeReadWrite => write!(f, "Balance tree update"),
44+
OutputMessage => write!(f, "Output message sent"),
45+
}
46+
}
47+
}
48+
49+
impl Effect {
50+
fn to_suggestion(&self) -> String {
51+
use Effect::*;
52+
String::from(match self {
53+
Interaction => "making all interactions",
54+
StorageWrite => "making all storage writes",
55+
StorageRead => "making all storage reads",
56+
BalanceTreeRead => "making all balance tree reads",
57+
BalanceTreeReadWrite => "making all balance tree updates",
58+
OutputMessage => "sending all output messages",
59+
})
60+
}
2661
}
2762

2863
// The algorithm that searches for storage operations after interaction
@@ -31,7 +66,7 @@ enum Effect {
3166
// storage reads or writes.
3267
enum CEIAnalysisState {
3368
LookingForInteraction, // initial state of the automaton
34-
LookingForStorageReadOrWrite,
69+
LookingForEffect,
3570
}
3671

3772
pub(crate) fn analyze_program(engines: Engines<'_>, prog: &ty::TyProgram) -> Vec<CompileWarning> {
@@ -100,7 +135,7 @@ fn impl_trait_methods<'a>(
100135
}
101136

102137
// This is the main part of the analysis algorithm:
103-
// we are looking for state effects after contract interaction
138+
// we are looking for various effects after contract interaction
104139
fn analyze_code_block(
105140
engines: Engines<'_>,
106141
codeblock: &ty::TyCodeBlock,
@@ -117,11 +152,11 @@ fn analyze_code_block(
117152
match analysis_state {
118153
CEIAnalysisState::LookingForInteraction => {
119154
if codeblock_entry_effects.contains(&Effect::Interaction) {
120-
analysis_state = CEIAnalysisState::LookingForStorageReadOrWrite;
155+
analysis_state = CEIAnalysisState::LookingForEffect;
121156
interaction_span = ast_node.span.clone();
122157
}
123158
}
124-
CEIAnalysisState::LookingForStorageReadOrWrite => warn_after_interaction(
159+
CEIAnalysisState::LookingForEffect => warn_after_interaction(
125160
&codeblock_entry_effects,
126161
&interaction_span,
127162
&ast_node.span,
@@ -306,27 +341,17 @@ fn analyze_expression(
306341
set_union(cond_then_effs, cond_else_effs)
307342
}
308343
WhileLoop { condition, body } => {
309-
// if the loop (condition + body) contains both interaction and storage operations
344+
// if the loop (condition + body) contains both interaction and state effects
310345
// in _any_ order, we report CEI pattern violation
311346
let cond_effs = analyze_expression(engines, condition, block_name, warnings);
312347
let body_effs = analyze_code_block(engines, body, block_name, warnings);
313348
let res_effs = set_union(cond_effs, body_effs);
314-
if res_effs.is_superset(&HashSet::from([Effect::Interaction, Effect::StorageRead])) {
315-
warnings.push(CompileWarning {
316-
span: expr.span.clone(),
317-
warning_content: Warning::StorageReadAfterInteraction {
318-
block_name: block_name.clone(),
319-
},
320-
});
321-
};
322-
if res_effs.is_superset(&HashSet::from([Effect::Interaction, Effect::StorageWrite])) {
323-
warnings.push(CompileWarning {
324-
span: expr.span.clone(),
325-
warning_content: Warning::StorageWriteAfterInteraction {
326-
block_name: block_name.clone(),
327-
},
328-
});
329-
};
349+
if res_effs.contains(&Effect::Interaction) {
350+
// TODO: the span is not very precise, we can do better here, but this
351+
// will need a bit of refactoring of the CEI analysis
352+
let span = expr.span.clone();
353+
warn_after_interaction(&res_effs, &span, &span, &block_name.clone(), warnings)
354+
}
330355
res_effs
331356
}
332357
AsmExpression {
@@ -386,11 +411,11 @@ fn analyze_expressions(
386411
match analysis_state {
387412
CEIAnalysisState::LookingForInteraction => {
388413
if expr_effs.contains(&Effect::Interaction) {
389-
analysis_state = CEIAnalysisState::LookingForStorageReadOrWrite;
414+
analysis_state = CEIAnalysisState::LookingForEffect;
390415
interaction_span = expr.span.clone();
391416
}
392417
}
393-
CEIAnalysisState::LookingForStorageReadOrWrite => warn_after_interaction(
418+
CEIAnalysisState::LookingForEffect => warn_after_interaction(
394419
&expr_effs,
395420
&interaction_span,
396421
&expr.span,
@@ -418,11 +443,11 @@ fn analyze_asm_block(
418443
match analysis_state {
419444
CEIAnalysisState::LookingForInteraction => {
420445
if asm_op_effs.contains(&Effect::Interaction) {
421-
analysis_state = CEIAnalysisState::LookingForStorageReadOrWrite;
446+
analysis_state = CEIAnalysisState::LookingForEffect;
422447
interaction_span = asm_op.span.clone();
423448
}
424449
}
425-
CEIAnalysisState::LookingForStorageReadOrWrite => warn_after_interaction(
450+
CEIAnalysisState::LookingForEffect => warn_after_interaction(
426451
&asm_op_effs,
427452
&interaction_span,
428453
&asm_op.span,
@@ -442,18 +467,14 @@ fn warn_after_interaction(
442467
block_name: &Ident,
443468
warnings: &mut Vec<CompileWarning>,
444469
) {
445-
if ast_node_effects.contains(&Effect::StorageRead) {
446-
warnings.push(CompileWarning {
447-
span: Span::join(interaction_span.clone(), effect_span.clone()),
448-
warning_content: Warning::StorageReadAfterInteraction {
449-
block_name: block_name.clone(),
450-
},
451-
});
452-
};
453-
if ast_node_effects.contains(&Effect::StorageWrite) {
470+
let interaction_singleton = HashSet::from([Effect::Interaction]);
471+
let state_effects = ast_node_effects.difference(&interaction_singleton);
472+
for eff in state_effects {
454473
warnings.push(CompileWarning {
455474
span: Span::join(interaction_span.clone(), effect_span.clone()),
456-
warning_content: Warning::StorageWriteAfterInteraction {
475+
warning_content: Warning::EffectAfterInteraction {
476+
effect: eff.to_string(),
477+
effect_in_suggestion: Effect::to_suggestion(eff),
457478
block_name: block_name.clone(),
458479
},
459480
});
@@ -593,15 +614,19 @@ fn effects_of_intrinsic(intr: &sway_ast::Intrinsic) -> HashSet<Effect> {
593614
match intr {
594615
StateStoreWord | StateStoreQuad => HashSet::from([Effect::StorageWrite]),
595616
StateLoadWord | StateLoadQuad => HashSet::from([Effect::StorageRead]),
617+
Smo => HashSet::from([Effect::OutputMessage]),
596618
Revert | IsReferenceType | SizeOfType | SizeOfVal | Eq | Gtf | AddrOf | Log | Add | Sub
597-
| Mul | Div | PtrAdd | PtrSub | GetStorageKey | Smo => HashSet::new(),
619+
| Mul | Div | PtrAdd | PtrSub | GetStorageKey => HashSet::new(),
598620
}
599621
}
600622

601623
fn effects_of_asm_op(op: &AsmOp) -> HashSet<Effect> {
602624
match op.op_name.as_str().to_lowercase().as_str() {
603625
"sww" | "swwq" => HashSet::from([Effect::StorageWrite]),
604-
"srw" | "srwq" | "bal" => HashSet::from([Effect::StorageRead]),
626+
"srw" | "srwq" => HashSet::from([Effect::StorageRead]),
627+
"tr" | "tro" => HashSet::from([Effect::BalanceTreeReadWrite]),
628+
"bal" => HashSet::from([Effect::BalanceTreeRead]),
629+
"smo" => HashSet::from([Effect::OutputMessage]),
605630
"call" => HashSet::from([Effect::Interaction]),
606631
// the rest of the assembly instructions are considered to not have effects
607632
_ => HashSet::new(),

sway-error/src/warning.rs

+6-8
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,9 @@ pub enum Warning {
9191
UnrecognizedAttribute {
9292
attrib_name: Ident,
9393
},
94-
StorageWriteAfterInteraction {
95-
block_name: Ident,
96-
},
97-
StorageReadAfterInteraction {
94+
EffectAfterInteraction {
95+
effect: String,
96+
effect_in_suggestion: String,
9897
block_name: Ident,
9998
},
10099
}
@@ -222,10 +221,9 @@ impl fmt::Display for Warning {
222221
),
223222
MatchExpressionUnreachableArm => write!(f, "This match arm is unreachable."),
224223
UnrecognizedAttribute {attrib_name} => write!(f, "Unknown attribute: \"{attrib_name}\"."),
225-
StorageWriteAfterInteraction {block_name} => write!(f, "Storage modification after external contract interaction in function or method \"{block_name}\". \
226-
Consider making all storage writes before calling another contract"),
227-
StorageReadAfterInteraction {block_name} => write!(f, "Storage read after external contract interaction in function or method \"{block_name}\". \
228-
Consider making all storage reads before calling another contract"),
224+
EffectAfterInteraction {effect, effect_in_suggestion, block_name} =>
225+
write!(f, "{effect} after external contract interaction in function or method \"{block_name}\". \
226+
Consider {effect_in_suggestion} before calling another contract"),
229227
}
230228
}
231229
}
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
category = "compile"
22

3-
# check: $()Storage modification after external contract interaction in function or method "deposit". Consider making all storage writes before calling another contract
3+
# check: $()Storage write after external contract interaction in function or method "deposit". Consider making all storage writes before calling another contract
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
category = "compile"
22

3-
# check: $()Storage modification after external contract interaction in function or method "deposit". Consider making all storage writes before calling another contract
3+
# check: $()Storage write after external contract interaction in function or method "deposit". Consider making all storage writes before calling another contract
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[[package]]
2+
name = 'cei_pattern_violation_in_asm_block_bal'
3+
source = 'member'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[project]
2+
name = "cei_pattern_violation_in_asm_block_bal"
3+
authors = ["Fuel Labs <[email protected]>"]
4+
entry = "main.sw"
5+
license = "Apache-2.0"
6+
implicit-std = false
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
contract;
2+
3+
abi TestAbi {
4+
fn deposit();
5+
}
6+
7+
impl TestAbi for Contract {
8+
fn deposit() {
9+
let other_contract = abi(TestAbi, 0x3dba0a4455b598b7655a7fb430883d96c9527ef275b49739e7b0ad12f8280eae);
10+
11+
// interaction
12+
other_contract.deposit();
13+
// effect -- therefore violation of CEI where effect should go before interaction
14+
asm(r1, r2: 0, r3: 0) {
15+
bal r1 r2 r3;
16+
}
17+
}
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
category = "compile"
2+
3+
# check: $()Balance tree read after external contract interaction in function or method "deposit". Consider making all balance tree reads before calling another contract
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[[package]]
2+
name = 'cei_pattern_violation_in_asm_block_smo'
3+
source = 'member'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[project]
2+
name = "cei_pattern_violation_in_asm_block_smo"
3+
authors = ["Fuel Labs <[email protected]>"]
4+
entry = "main.sw"
5+
license = "Apache-2.0"
6+
implicit-std = false
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
contract;
2+
3+
abi TestAbi {
4+
fn deposit();
5+
}
6+
7+
impl TestAbi for Contract {
8+
fn deposit() {
9+
let other_contract = abi(TestAbi, 0x3dba0a4455b598b7655a7fb430883d96c9527ef275b49739e7b0ad12f8280eae);
10+
11+
// interaction
12+
other_contract.deposit();
13+
// effect -- therefore violation of CEI where effect should go before interaction
14+
asm(r1: 0, r2: 0, r3: 0, r4: 0) {
15+
smo r1 r2 r3 r4;
16+
}
17+
}
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
category = "compile"
2+
3+
# check: $()Output message sent after external contract interaction in function or method "deposit". Consider sending all output messages before calling another contract
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[[package]]
2+
name = 'cei_pattern_violation_in_asm_block_tr'
3+
source = 'member'
4+
dependencies = ['std']
5+
6+
[[package]]
7+
name = 'core'
8+
source = 'path+from-root-1F667E1725DAD261'
9+
10+
[[package]]
11+
name = 'std'
12+
source = 'path+from-root-1F667E1725DAD261'
13+
dependencies = ['core']
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[project]
2+
name = "cei_pattern_violation_in_asm_block_tr"
3+
authors = ["Fuel Labs <[email protected]>"]
4+
entry = "main.sw"
5+
license = "Apache-2.0"
6+
7+
[dependencies]
8+
std = { path = "../../../../../../../sway-lib-std" }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
contract;
2+
3+
use std::token::force_transfer_to_contract;
4+
5+
abi TestAbi {
6+
fn deposit();
7+
}
8+
9+
impl TestAbi for Contract {
10+
fn deposit() {
11+
let other_contract = abi(TestAbi, 0x3dba0a4455b598b7655a7fb430883d96c9527ef275b49739e7b0ad12f8280eae);
12+
13+
// interaction
14+
other_contract.deposit();
15+
// effect -- therefore violation of CEI where effect should go before interaction
16+
let amount = 10;
17+
let address = 0x0000000000000000000000000000000000000000000000000000000000000001;
18+
let asset = ContractId::from(address);
19+
let pool = ContractId::from(address);
20+
// `force_transfer_to_contract` uses `tr` asm instruction
21+
force_transfer_to_contract(amount, asset, pool);
22+
}
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
category = "compile"
2+
3+
# check: $()Balance tree update after external contract interaction in function or method "deposit". Consider making all balance tree updates before calling another contract
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[[package]]
2+
name = 'cei_pattern_violation_in_asm_block_tro'
3+
source = 'member'
4+
dependencies = ['std']
5+
6+
[[package]]
7+
name = 'core'
8+
source = 'path+from-root-D9AC6CFA47996221'
9+
10+
[[package]]
11+
name = 'std'
12+
source = 'path+from-root-D9AC6CFA47996221'
13+
dependencies = ['core']
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[project]
2+
name = "cei_pattern_violation_in_asm_block_tro"
3+
authors = ["Fuel Labs <[email protected]>"]
4+
entry = "main.sw"
5+
license = "Apache-2.0"
6+
7+
[dependencies]
8+
std = { path = "../../../../../../../sway-lib-std" }

0 commit comments

Comments
 (0)