fuels_programs/assembly/
contract_call.rs

1use fuel_asm::{op, Instruction, RegId, Word};
2use fuel_tx::{AssetId, ContractId};
3use fuels_core::{constants::WORD_SIZE, error, types::errors::Result};
4
5use super::cursor::WasmFriendlyCursor;
6#[derive(Debug)]
7pub struct ContractCallInstructions {
8    instructions: Vec<Instruction>,
9    gas_fwd: bool,
10}
11
12impl IntoIterator for ContractCallInstructions {
13    type Item = Instruction;
14    type IntoIter = std::vec::IntoIter<Instruction>;
15    fn into_iter(self) -> Self::IntoIter {
16        self.instructions.into_iter()
17    }
18}
19
20impl ContractCallInstructions {
21    pub fn new(opcode_params: CallOpcodeParamsOffset) -> Self {
22        Self {
23            gas_fwd: opcode_params.gas_forwarded_offset.is_some(),
24            instructions: Self::generate_instructions(opcode_params),
25        }
26    }
27
28    pub fn into_bytes(self) -> impl Iterator<Item = u8> {
29        self.instructions
30            .into_iter()
31            .flat_map(|instruction| instruction.to_bytes())
32    }
33
34    /// Returns the VM instructions for calling a contract method
35    /// We use the [`Opcode`] to call a contract: [`CALL`](Opcode::CALL)
36    /// pointing at the following registers:
37    ///
38    /// 0x10 Script data offset
39    /// 0x11 Coin amount
40    /// 0x12 Asset ID
41    /// 0x13 Gas forwarded
42    ///
43    /// Note that these are soft rules as we're picking this addresses simply because they
44    /// non-reserved register.
45    fn generate_instructions(offsets: CallOpcodeParamsOffset) -> Vec<Instruction> {
46        let call_data_offset = offsets
47            .call_data_offset
48            .try_into()
49            .expect("call_data_offset out of range");
50        let amount_offset = offsets
51            .amount_offset
52            .try_into()
53            .expect("amount_offset out of range");
54        let asset_id_offset = offsets
55            .asset_id_offset
56            .try_into()
57            .expect("asset_id_offset out of range");
58
59        let mut instructions = [
60            op::movi(0x10, call_data_offset),
61            op::movi(0x11, amount_offset),
62            op::lw(0x11, 0x11, 0),
63            op::movi(0x12, asset_id_offset),
64        ]
65        .to_vec();
66
67        match offsets.gas_forwarded_offset {
68            Some(gas_forwarded_offset) => {
69                let gas_forwarded_offset = gas_forwarded_offset
70                    .try_into()
71                    .expect("gas_forwarded_offset out of range");
72
73                instructions.extend(&[
74                    op::movi(0x13, gas_forwarded_offset),
75                    op::lw(0x13, 0x13, 0),
76                    op::call(0x10, 0x11, 0x12, 0x13),
77                ]);
78            }
79            // if `gas_forwarded` was not set use `REG_CGAS`
80            None => instructions.push(op::call(0x10, 0x11, 0x12, RegId::CGAS)),
81        };
82
83        instructions
84    }
85
86    fn extract_normal_variant(instructions: &[Instruction]) -> Option<&[Instruction]> {
87        let normal_instructions = Self::generate_instructions(CallOpcodeParamsOffset {
88            call_data_offset: 0,
89            amount_offset: 0,
90            asset_id_offset: 0,
91            gas_forwarded_offset: None,
92        });
93        Self::extract_if_match(instructions, &normal_instructions)
94    }
95
96    fn extract_gas_fwd_variant(instructions: &[Instruction]) -> Option<&[Instruction]> {
97        let gas_fwd_instructions = Self::generate_instructions(CallOpcodeParamsOffset {
98            call_data_offset: 0,
99            amount_offset: 0,
100            asset_id_offset: 0,
101            gas_forwarded_offset: Some(0),
102        });
103        Self::extract_if_match(instructions, &gas_fwd_instructions)
104    }
105
106    pub fn extract_from(instructions: &[Instruction]) -> Option<Self> {
107        if let Some(instructions) = Self::extract_normal_variant(instructions) {
108            return Some(Self {
109                instructions: instructions.to_vec(),
110                gas_fwd: false,
111            });
112        }
113
114        Self::extract_gas_fwd_variant(instructions).map(|instructions| Self {
115            instructions: instructions.to_vec(),
116            gas_fwd: true,
117        })
118    }
119
120    pub fn len(&self) -> usize {
121        self.instructions.len()
122    }
123
124    pub fn call_data_offset(&self) -> u32 {
125        let Instruction::MOVI(movi) = self.instructions[0] else {
126            panic!("should have validated the first instruction is a MOVI");
127        };
128
129        movi.imm18().into()
130    }
131
132    pub fn is_gas_fwd_variant(&self) -> bool {
133        self.gas_fwd
134    }
135
136    fn extract_if_match<'a>(
137        unknown: &'a [Instruction],
138        correct: &[Instruction],
139    ) -> Option<&'a [Instruction]> {
140        if unknown.len() < correct.len() {
141            return None;
142        }
143
144        unknown
145            .iter()
146            .zip(correct)
147            .all(|(expected, actual)| expected.opcode() == actual.opcode())
148            .then(|| &unknown[..correct.len()])
149    }
150}
151
152#[derive(Debug, Clone, PartialEq, Eq)]
153pub struct ContractCallData {
154    pub amount: u64,
155    pub asset_id: AssetId,
156    pub contract_id: ContractId,
157    pub fn_selector_encoded: Vec<u8>,
158    pub encoded_args: Vec<u8>,
159    pub gas_forwarded: Option<u64>,
160}
161
162impl ContractCallData {
163    pub fn decode_fn_selector(&self) -> Result<String> {
164        String::from_utf8(self.fn_selector_encoded.clone())
165            .map_err(|e| error!(Codec, "cannot decode function selector: {}", e))
166    }
167
168    /// Encodes as script data, consisting of the following items in the given order:
169    /// 1. Amount to be forwarded `(1 * `[`WORD_SIZE`]`)`
170    /// 2. Asset ID to be forwarded ([`AssetId::LEN`])
171    /// 3. Contract ID ([`ContractId::LEN`]);
172    /// 4. Function selector offset `(1 * `[`WORD_SIZE`]`)`
173    /// 5. Calldata offset `(1 * `[`WORD_SIZE`]`)`
174    /// 6. Encoded function selector - method name
175    /// 7. Encoded arguments
176    /// 8. Gas to be forwarded `(1 * `[`WORD_SIZE`]`)` - Optional
177    pub fn encode(&self, memory_offset: usize, buffer: &mut Vec<u8>) -> CallOpcodeParamsOffset {
178        let amount_offset = memory_offset;
179        let asset_id_offset = amount_offset + WORD_SIZE;
180        let call_data_offset = asset_id_offset + AssetId::LEN;
181        let encoded_selector_offset = call_data_offset + ContractId::LEN + 2 * WORD_SIZE;
182        let encoded_args_offset = encoded_selector_offset + self.fn_selector_encoded.len();
183
184        buffer.extend(self.amount.to_be_bytes()); // 1. Amount
185
186        let asset_id = self.asset_id;
187        buffer.extend(asset_id.iter()); // 2. Asset ID
188
189        buffer.extend(self.contract_id.as_ref()); // 3. Contract ID
190
191        buffer.extend((encoded_selector_offset as Word).to_be_bytes()); // 4. Fun. selector offset
192
193        buffer.extend((encoded_args_offset as Word).to_be_bytes()); // 5. Calldata offset
194
195        buffer.extend(&self.fn_selector_encoded); // 6. Encoded function selector
196
197        let encoded_args_len = self.encoded_args.len();
198
199        buffer.extend(&self.encoded_args); // 7. Encoded arguments
200
201        let gas_forwarded_offset = self.gas_forwarded.map(|gf| {
202            buffer.extend((gf as Word).to_be_bytes()); // 8. Gas to be forwarded - Optional
203
204            encoded_args_offset + encoded_args_len
205        });
206
207        CallOpcodeParamsOffset {
208            amount_offset,
209            asset_id_offset,
210            gas_forwarded_offset,
211            call_data_offset,
212        }
213    }
214
215    pub fn decode(data: &[u8], gas_fwd: bool) -> Result<Self> {
216        let mut data = WasmFriendlyCursor::new(data);
217
218        let amount = u64::from_be_bytes(data.consume_fixed("amount")?);
219
220        let asset_id = AssetId::new(data.consume_fixed("asset id")?);
221
222        let contract_id = ContractId::new(data.consume_fixed("contract id")?);
223
224        let _ = data.consume(8, "function selector offset")?;
225
226        let _ = data.consume(8, "encoded args offset")?;
227
228        let fn_selector = {
229            let fn_selector_len = {
230                let bytes = data.consume_fixed("function selector length")?;
231                u64::from_be_bytes(bytes) as usize
232            };
233            data.consume(fn_selector_len, "function selector")?.to_vec()
234        };
235
236        let (encoded_args, gas_forwarded) = if gas_fwd {
237            let encoded_args = data
238                .consume(data.unconsumed().saturating_sub(WORD_SIZE), "encoded_args")?
239                .to_vec();
240
241            let gas_fwd = { u64::from_be_bytes(data.consume_fixed("forwarded gas")?) };
242
243            (encoded_args, Some(gas_fwd))
244        } else {
245            (data.consume_all().to_vec(), None)
246        };
247
248        Ok(ContractCallData {
249            amount,
250            asset_id,
251            contract_id,
252            fn_selector_encoded: fn_selector,
253            encoded_args,
254            gas_forwarded,
255        })
256    }
257}
258
259#[derive(Default)]
260/// Specifies offsets of [`Opcode::CALL`][`fuel_asm::Opcode::CALL`] parameters stored in the script
261/// data from which they can be loaded into registers
262pub struct CallOpcodeParamsOffset {
263    pub call_data_offset: usize,
264    pub amount_offset: usize,
265    pub asset_id_offset: usize,
266    pub gas_forwarded_offset: Option<usize>,
267}
268
269// Creates a contract that loads the specified blobs into memory and delegates the call to the code contained in the blobs.
270pub fn loader_contract_asm(blob_ids: &[[u8; 32]]) -> Result<Vec<u8>> {
271    const BLOB_ID_SIZE: u16 = 32;
272    let get_instructions = |num_of_instructions, num_of_blobs| {
273        // There are 2 main steps:
274        // 1. Load the blob contents into memory
275        // 2. Jump to the beginning of the memory where the blobs were loaded
276        // After that the execution continues normally with the loaded contract reading our
277        // prepared fn selector and jumps to the selected contract method.
278        [
279            // 1. Load the blob contents into memory
280            // Find the start of the hardcoded blob IDs, which are located after the code ends.
281            op::move_(0x10, RegId::PC),
282            // 0x10 to hold the address of the current blob ID.
283            op::addi(0x10, 0x10, num_of_instructions * Instruction::SIZE as u16),
284            // The contract is going to be loaded from the current value of SP onwards, save
285            // the location into 0x16 so we can jump into it later on.
286            op::move_(0x16, RegId::SP),
287            // Loop counter.
288            op::movi(0x13, num_of_blobs),
289            // LOOP starts here.
290            // 0x11 to hold the size of the current blob.
291            op::bsiz(0x11, 0x10),
292            // Push the blob contents onto the stack.
293            op::ldc(0x10, 0, 0x11, 1),
294            // Move on to the next blob.
295            op::addi(0x10, 0x10, BLOB_ID_SIZE),
296            // Decrement the loop counter.
297            op::subi(0x13, 0x13, 1),
298            // Jump backwards (3+1) instructions if the counter has not reached 0.
299            op::jnzb(0x13, RegId::ZERO, 3),
300            // 2. Jump into the memory where the contract is loaded.
301            // What follows is called _jmp_mem by the sway compiler.
302            // Subtract the address contained in IS because jmp will add it back.
303            op::sub(0x16, 0x16, RegId::IS),
304            // jmp will multiply by 4, so we need to divide to cancel that out.
305            op::divi(0x16, 0x16, 4),
306            // Jump to the start of the contract we loaded.
307            op::jmp(0x16),
308        ]
309    };
310
311    let num_of_instructions = u16::try_from(get_instructions(0, 0).len())
312        .expect("to never have more than u16::MAX instructions");
313
314    let num_of_blobs = u32::try_from(blob_ids.len()).map_err(|_| {
315        error!(
316            Other,
317            "the number of blobs ({}) exceeds the maximum number of blobs supported: {}",
318            blob_ids.len(),
319            u32::MAX
320        )
321    })?;
322
323    let instruction_bytes = get_instructions(num_of_instructions, num_of_blobs)
324        .into_iter()
325        .flat_map(|instruction| instruction.to_bytes());
326
327    let blob_bytes = blob_ids.iter().flatten().copied();
328
329    Ok(instruction_bytes.chain(blob_bytes).collect())
330}