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