Skip to content

Commit 39d25b6

Browse files
authored
fallback when all contract selectors fail (#5703)
## Description This PR closes #5566. Contracts now can have a special function decorated with `#[fallback]` which is called when the contract method selection fails. This function for all intents and purposes works as a standard contract method, so: - it cannot call others contracts methods; - it has the same limitations of inputs and outputs. This function can return a value like a normal contract would, or it can use the `__contract_ret` intrinsics to return any value. ## Checklist - [x] I have linked to any relevant issues. - [x] I have commented my code, particularly in hard-to-understand areas. - [ ] I have updated the documentation where relevant (API docs, the reference, and the Sway book). - [x] I have added tests that prove my fix is effective or that my feature works. - [x] I have added (or requested a maintainer to add) the necessary `Breaking*` or `New Feature` labels where relevant. - [x] I have done my best to ensure that my PR adheres to [the Fuel Labs Code Review Standards](https://github.com/FuelLabs/rfcs/blob/master/text/code-standards/external-contributors.md). - [x] I have requested a review from the relevant team or maintainers.
1 parent 63cee46 commit 39d25b6

File tree

51 files changed

+394
-97
lines changed

Some content is hidden

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

51 files changed

+394
-97
lines changed

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

+28
Original file line numberDiff line numberDiff line change
@@ -163,3 +163,31 @@ While the Fuel contract calling paradigm is similar to the EVM's (using an ABI,
163163
1. [**Native assets**](./native_assets.md): FuelVM calls can forward any native asset not just base asset.
164164
165165
2. **No data serialization**: Contract calls in the FuelVM do not need to serialize data to pass it between contracts; instead they simply pass a pointer to the data. This is because the FuelVM has a shared global memory which all call frames can read from.
166+
167+
## Fallback
168+
169+
When a contract is compiled, a special section called "contract selection" is also generated. This section checks if the contract call method matches any of the available ABI methods. If this fails, one of two possible actions will happen:
170+
171+
1 - if no fallback function was specified, the contract will revert;
172+
2 - otherwise, the fallback function will be called.
173+
174+
For all intents and purposes the fallback function is considered a contract method, which means that it has all the limitations that other contract methods have. As the fallback function signature, the function cannot have arguments, but they can return anything.
175+
176+
If for some reason the fallback function needs to returns different types, the intrinsic `__contract_ret` can be used.
177+
178+
```sway
179+
contract;
180+
181+
abi MyContract {
182+
fn some_method();
183+
}
184+
185+
impl ContractB for Contract {
186+
fn some_method() {
187+
}
188+
}
189+
190+
#[fallback]
191+
fn fallback() {
192+
}
193+
```

docs/book/src/reference/attributes.md

+4
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,7 @@ More details in [Unit Testing](../testing/unit-testing.md).
5959
The `#[deprecated]` attribute marks an item as deprecated and makes the compiler emit a warning for every usage of the deprecated item. This warning can be disabled using `#[allow(deprecated)]`.
6060

6161
It is possible to improve the warning message with `#[deprecated(note = "your message")]`
62+
63+
## Fallback
64+
65+
The `#[fallback]` attribute makes the compiler use the marked function as the contract call fallback function, which means that, when a contract is called, and the contract selection fails, the fallback function will be called instead.

sway-core/src/asm_generation/from_ir.rs

+7-2
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,15 @@ fn compile_module_to_asm(
7676
BuildTarget::MidenVM => Box::new(MidenVMAsmBuilder::new(kind, context)),
7777
};
7878

79+
let mut fallback_fn = None;
80+
7981
// Pre-create labels for all functions before we generate other code, so we can call them
8082
// before compiling them if needed.
8183
for func in module.function_iter(context) {
82-
builder.func_to_labels(&func);
84+
let (start, _) = builder.func_to_labels(&func);
85+
if func.is_fallback(context) {
86+
fallback_fn = Some(start);
87+
}
8388
}
8489

8590
for function in module.function_iter(context) {
@@ -126,7 +131,7 @@ fn compile_module_to_asm(
126131
}
127132

128133
let allocated_program = abstract_program
129-
.into_allocated_program()
134+
.into_allocated_program(fallback_fn)
130135
.map_err(|e| handler.emit_err(e))?;
131136

132137
if build_config

sway-core/src/asm_generation/programs/abstract.rs

+18-5
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,10 @@ impl AbstractProgram {
3939
}
4040
}
4141

42-
pub(crate) fn into_allocated_program(mut self) -> Result<AllocatedProgram, CompileError> {
42+
pub(crate) fn into_allocated_program(
43+
mut self,
44+
fallback_fn: Option<crate::asm_lang::Label>,
45+
) -> Result<AllocatedProgram, CompileError> {
4346
// Build our bytecode prologue which has a preamble and for contracts is the switch based on
4447
// function selector.
4548
let mut prologue = self.build_preamble();
@@ -49,7 +52,7 @@ impl AbstractProgram {
4952
self.build_jump_to_entry(&mut prologue);
5053
}
5154
(false, ProgramKind::Contract) => {
52-
self.build_contract_abi_switch(&mut prologue);
55+
self.build_contract_abi_switch(&mut prologue, fallback_fn);
5356
}
5457
_ => {}
5558
}
@@ -172,7 +175,11 @@ impl AbstractProgram {
172175
/// 'selector'.
173176
/// See https://fuellabs.github.io/fuel-specs/master/vm#call-frames which
174177
/// describes the first argument to be at word offset 73.
175-
fn build_contract_abi_switch(&mut self, asm_buf: &mut AllocatedAbstractInstructionSet) {
178+
fn build_contract_abi_switch(
179+
&mut self,
180+
asm_buf: &mut AllocatedAbstractInstructionSet,
181+
fallback_fn: Option<crate::asm_lang::Label>,
182+
) {
176183
const SELECTOR_WORD_OFFSET: u64 = 73;
177184
const INPUT_SELECTOR_REG: AllocatedRegister = AllocatedRegister::Allocated(0);
178185
const PROG_SELECTOR_REG: AllocatedRegister = AllocatedRegister::Allocated(1);
@@ -241,8 +248,14 @@ impl AbstractProgram {
241248
});
242249
}
243250

244-
// If none of the selectors matched, then revert. This may change in the future, see
245-
// https://github.com/FuelLabs/sway/issues/444
251+
if let Some(fallback_fn) = fallback_fn {
252+
asm_buf.ops.push(AllocatedAbstractOp {
253+
opcode: Either::Right(ControlFlowOp::Call(fallback_fn)),
254+
comment: "call fallback function".into(),
255+
owning_span: None,
256+
});
257+
}
258+
246259
asm_buf.ops.push(AllocatedAbstractOp {
247260
opcode: Either::Left(AllocatedOpcode::MOVI(
248261
AllocatedRegister::Constant(ConstantRegister::Scratch),

sway-core/src/control_flow_analysis/dead_code_analysis.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ use sway_types::{
2727
Ident, Named, Spanned,
2828
};
2929

30-
// Defines if this node starts the dca graph or not
30+
// Defines if this node is a root in the dca graph or not
3131
fn is_entry_point(node: &TyAstNode, decl_engine: &DeclEngine, tree_type: &TreeType) -> bool {
3232
match tree_type {
3333
TreeType::Predicate | TreeType::Script => {
@@ -59,7 +59,7 @@ fn is_entry_point(node: &TyAstNode, decl_engine: &DeclEngine, tree_type: &TreeTy
5959
..
6060
} => {
6161
let decl = decl_engine.get_function(decl_id);
62-
decl.visibility == Visibility::Public || decl.is_test()
62+
decl.visibility == Visibility::Public || decl.is_test() || decl.is_fallback()
6363
}
6464
TyAstNode {
6565
content:

sway-core/src/ir_generation/compile.rs

+23-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::{
2-
decl_engine::{DeclId, DeclRefFunction},
2+
decl_engine::{DeclEngineGet, DeclId, DeclRefFunction},
33
language::{ty, Visibility},
44
metadata::MetadataManager,
55
semantic_analysis::namespace,
@@ -140,13 +140,13 @@ pub(super) fn compile_contract(
140140
)
141141
.map_err(|err| vec![err])?;
142142

143-
if let Some(main_function) = entry_function {
143+
if let Some(entry_function) = entry_function {
144144
compile_entry_function(
145145
engines,
146146
context,
147147
&mut md_mgr,
148148
module,
149-
main_function,
149+
entry_function,
150150
logged_types_map,
151151
messages_types_map,
152152
None,
@@ -165,6 +165,25 @@ pub(super) fn compile_contract(
165165
)?;
166166
}
167167

168+
// Fallback function needs to be compiled
169+
for decl in declarations {
170+
if let ty::TyDecl::FunctionDecl(decl) = decl {
171+
let decl_id = decl.decl_id;
172+
let decl = engines.de().get(&decl_id);
173+
if decl.is_fallback() {
174+
compile_abi_method(
175+
context,
176+
&mut md_mgr,
177+
module,
178+
&decl_id,
179+
logged_types_map,
180+
messages_types_map,
181+
engines,
182+
)?;
183+
}
184+
}
185+
}
186+
168187
compile_tests(
169188
engines,
170189
context,
@@ -504,6 +523,7 @@ fn compile_fn(
504523
selector,
505524
*visibility == Visibility::Public,
506525
is_entry,
526+
ast_fn_decl.is_fallback(),
507527
metadata,
508528
);
509529

sway-core/src/language/ty/declaration/function.rs

+5
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use sway_error::handler::{ErrorEmitted, Handler};
1010
use crate::{
1111
language::{parsed::FunctionDeclarationKind, CallPath},
1212
semantic_analysis::type_check_context::MonomorphizeHelper,
13+
transform::AttributeKind,
1314
};
1415

1516
use crate::{
@@ -441,6 +442,10 @@ impl TyFunctionDecl {
441442
}
442443
}
443444

445+
pub fn is_fallback(&self) -> bool {
446+
self.attributes.contains_key(&AttributeKind::Fallback)
447+
}
448+
444449
/// Whether or not this function is a constructor for the type given by `type_id`.
445450
///
446451
/// Returns `Some(true)` if the function is surely the constructor and `Some(false)` if

sway-core/src/semantic_analysis/ast_node/declaration/auto_impl.rs

+15-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::{
2+
asm_generation::fuel::compiler_constants::MISMATCHED_SELECTOR_REVERT_CODE,
23
decl_engine::{DeclEngineGet, DeclId, DeclRef},
34
language::{
45
parsed::{self, AstNodeContent, Declaration, FunctionDeclarationKind},
@@ -513,6 +514,7 @@ where
513514
engines: &Engines,
514515
module_id: Option<ModuleId>,
515516
contract_fns: &[DeclRef<DeclId<TyFunctionDecl>>],
517+
fallback_fn: Option<DeclId<TyFunctionDecl>>,
516518
) -> Option<TyAstNode> {
517519
let mut code = String::new();
518520

@@ -581,15 +583,24 @@ where
581583
}
582584
.into();
583585

586+
let fallback = if let Some(fallback_fn) = fallback_fn {
587+
let fallback_fn = engines.de().get(&fallback_fn);
588+
let return_type = Self::generate_type(engines, fallback_fn.return_type.type_id);
589+
let method_name = fallback_fn.name.as_str();
590+
591+
format!("let result: raw_slice = encode::<{return_type}>({method_name}()); __contract_ret(result.ptr(), result.len::<u8>());")
592+
} else {
593+
// as the old encoding does
594+
format!("__revert({});", MISMATCHED_SELECTOR_REVERT_CODE)
595+
};
596+
584597
let code = format!(
585-
"{att}
586-
pub fn __entry() {{
598+
"{att} pub fn __entry() {{
587599
let method_name = decode_first_param::<str>();
588-
__log(method_name);
589600
{code}
601+
{fallback}
590602
}}"
591603
);
592-
593604
self.parse_item_fn_to_typed_ast_node(
594605
engines,
595606
module_id,

sway-core/src/semantic_analysis/module.rs

+60-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use sway_error::{
1212
use sway_types::{BaseIdent, Named};
1313

1414
use crate::{
15-
decl_engine::DeclEngineGet,
15+
decl_engine::{DeclEngineGet, DeclId},
1616
engine_threading::DebugWithEngines,
1717
language::{
1818
parsed::*,
@@ -307,6 +307,17 @@ impl ty::TyModule {
307307
let mut all_nodes = Self::type_check_nodes(handler, ctx.by_ref(), ordered_nodes)?;
308308
let submodules = submodules_res?;
309309

310+
let fallback_fn = collect_fallback_fn(&all_nodes, engines, handler)?;
311+
match (&kind, &fallback_fn) {
312+
(TreeType::Contract, _) | (_, None) => {}
313+
(_, Some(fallback_fn)) => {
314+
let fallback_fn = engines.de().get(fallback_fn);
315+
return Err(handler.emit_err(CompileError::FallbackFnsAreContractOnly {
316+
span: fallback_fn.span.clone(),
317+
}));
318+
}
319+
}
320+
310321
if ctx.experimental.new_encoding {
311322
let main_decl = all_nodes.iter_mut().find_map(|x| match &mut x.content {
312323
ty::TyAstNodeContent::Declaration(ty::TyDecl::FunctionDecl(decl)) => {
@@ -348,6 +359,7 @@ impl ty::TyModule {
348359
engines,
349360
parsed.span.source_id().map(|x| x.module_id()),
350361
&contract_fns,
362+
fallback_fn,
351363
)
352364
.unwrap();
353365
all_nodes.push(node)
@@ -463,6 +475,53 @@ impl ty::TyModule {
463475
}
464476
}
465477

478+
fn collect_fallback_fn(
479+
all_nodes: &[ty::TyAstNode],
480+
engines: &Engines,
481+
handler: &Handler,
482+
) -> Result<Option<DeclId<ty::TyFunctionDecl>>, ErrorEmitted> {
483+
let mut fallback_fns = all_nodes
484+
.iter()
485+
.filter_map(|x| match &x.content {
486+
ty::TyAstNodeContent::Declaration(ty::TyDecl::FunctionDecl(decl)) => {
487+
let d = engines.de().get(&decl.decl_id);
488+
d.is_fallback().then_some(decl.decl_id)
489+
}
490+
_ => None,
491+
})
492+
.collect::<Vec<_>>();
493+
494+
let mut last_error = None;
495+
for f in fallback_fns.iter().skip(1) {
496+
let decl = engines.de().get(f);
497+
last_error = Some(
498+
handler.emit_err(CompileError::MultipleDefinitionsOfFallbackFunction {
499+
name: decl.name.clone(),
500+
span: decl.span.clone(),
501+
}),
502+
);
503+
}
504+
505+
if let Some(last_error) = last_error {
506+
return Err(last_error);
507+
}
508+
509+
if let Some(fallback_fn) = fallback_fns.pop() {
510+
let f = engines.de().get(&fallback_fn);
511+
if !f.parameters.is_empty() {
512+
Err(
513+
handler.emit_err(CompileError::FallbackFnsCannotHaveParameters {
514+
span: f.span.clone(),
515+
}),
516+
)
517+
} else {
518+
Ok(Some(fallback_fn))
519+
}
520+
} else {
521+
Ok(None)
522+
}
523+
}
524+
466525
impl ty::TySubmodule {
467526
pub fn build_dep_graph(
468527
_handler: &Handler,

sway-core/src/transform/attribute.rs

+6-2
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ pub enum AttributeKind {
6868
Cfg,
6969
Deprecated,
7070
Namespace,
71+
Fallback,
7172
}
7273

7374
impl AttributeKind {
@@ -76,7 +77,9 @@ impl AttributeKind {
7677
pub fn expected_args_len_min_max(self) -> (usize, Option<usize>) {
7778
use AttributeKind::*;
7879
match self {
79-
Doc | DocComment | Storage | Inline | Test | Payable | Deprecated => (0, None),
80+
Doc | DocComment | Storage | Inline | Test | Payable | Deprecated | Fallback => {
81+
(0, None)
82+
}
8083
Allow | Cfg | Namespace => (1, Some(1)),
8184
}
8285
}
@@ -85,7 +88,8 @@ impl AttributeKind {
8588
pub fn expected_args_values(self, _arg_index: usize) -> Option<Vec<String>> {
8689
use AttributeKind::*;
8790
match self {
88-
Deprecated | Namespace | Doc | DocComment | Storage | Inline | Test | Payable => None,
91+
Deprecated | Namespace | Doc | DocComment | Storage | Inline | Test | Payable
92+
| Fallback => None,
8993
Allow => Some(vec![
9094
ALLOW_DEAD_CODE_NAME.to_string(),
9195
ALLOW_DEPRECATED_NAME.to_string(),

sway-core/src/transform/to_parsed_lang/convert_parse_tree.rs

+5-4
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@ use sway_types::{
3232
constants::{
3333
ALLOW_ATTRIBUTE_NAME, CFG_ATTRIBUTE_NAME, CFG_EXPERIMENTAL_NEW_ENCODING,
3434
CFG_PROGRAM_TYPE_ARG_NAME, CFG_TARGET_ARG_NAME, DEPRECATED_ATTRIBUTE_NAME,
35-
DOC_ATTRIBUTE_NAME, DOC_COMMENT_ATTRIBUTE_NAME, INLINE_ATTRIBUTE_NAME,
36-
NAMESPACE_ATTRIBUTE_NAME, PAYABLE_ATTRIBUTE_NAME, STORAGE_PURITY_ATTRIBUTE_NAME,
37-
STORAGE_PURITY_READ_NAME, STORAGE_PURITY_WRITE_NAME, TEST_ATTRIBUTE_NAME,
38-
VALID_ATTRIBUTE_NAMES,
35+
DOC_ATTRIBUTE_NAME, DOC_COMMENT_ATTRIBUTE_NAME, FALLBACK_ATTRIBUTE_NAME,
36+
INLINE_ATTRIBUTE_NAME, NAMESPACE_ATTRIBUTE_NAME, PAYABLE_ATTRIBUTE_NAME,
37+
STORAGE_PURITY_ATTRIBUTE_NAME, STORAGE_PURITY_READ_NAME, STORAGE_PURITY_WRITE_NAME,
38+
TEST_ATTRIBUTE_NAME, VALID_ATTRIBUTE_NAMES,
3939
},
4040
integer_bits::IntegerBits,
4141
};
@@ -4544,6 +4544,7 @@ fn item_attrs_to_map(
45444544
CFG_ATTRIBUTE_NAME => Some(AttributeKind::Cfg),
45454545
DEPRECATED_ATTRIBUTE_NAME => Some(AttributeKind::Deprecated),
45464546
NAMESPACE_ATTRIBUTE_NAME => Some(AttributeKind::Namespace),
4547+
FALLBACK_ATTRIBUTE_NAME => Some(AttributeKind::Fallback),
45474548
_ => None,
45484549
} {
45494550
match attrs_map.get_mut(&attr_kind) {

0 commit comments

Comments
 (0)