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