Problem
Fuel’s unique transaction model provides substantially more flexibility for transactions, yet introduces some challenges. These challenges are particularly derived from Fuel’s multi-input model, as well as native support for alternative signature schemes using predicates.
One issue that has become apparent recently is the lack of a canonical “sender” of a transaction, breaking from Ethereum’s msg_sender
model. More generally, Fuel applications are limited by the fact that applications can’t expect a unified signature scheme from all accounts (since predicate implementations may vary).
This document proposes a simple solution to address these issues.
Solution: “note” inputs
Transaction inputs represent the primary mechanism for authorizing the various roles and actions of a transaction. Therefore, it’s sensible to extend the functionality of the “input” set to support this new funcitonality.
A new type of input Note
would be added to the existing input types (Coin
, Contract
and Message
). Notes represent an arbitrary piece of data that is “signed” by a given account.
The InputNote
would have the following structure:
name | type | description |
---|---|---|
owner |
byte[32] |
The address or predicate that is “authorizing” the predicate. |
noteId |
byte[32] |
An identifier for the type of note. |
witnessIndex |
uint16 |
Index of witness that authorizes spending the coin. |
predicateGasUsed |
uint64 |
Gas used by predicate execution. |
dataLength |
uint64 |
Length of note data, in bytes. |
predicateLength |
uint64 |
Length of predicate, in instructions. |
predicateDataLength |
uint64 |
Length of predicate input data, in bytes. |
data |
byte[] |
The note data. |
predicate |
byte[] |
Predicate bytecode. |
predicateData |
byte[] |
Predicate input data (parameters). |
Any account can include arbitrary notes in their transactions. Notes follow the same authentication rules as Coin
and Message
inputs (they must either be attached to witness signature of the full transaction ID, or approved by a predicate). However, given that Notes
are not the outputs of previous transactions, they can be included arbitrarily.
Use Cases
Ownership of the script execution (msg_sender
)
Problem
The Fuel standard library provides a function msg_sender
, which aims to replicate the utility of msg.sender
in Solidity. However, the Fuel transaction model makes this functionality less straightforward than in the EVM. While EVM transactions have a single “sender”, Fuel’s transaction model allows for multiple inputs to from various addresses to be included.
The current implementation of msg_sender
simply checks the owner of all Coin
and Message
inputs. If all inputs have matching owners, then that address is returned, otherwise an error is returned.
This is problematic for applications that want to use multiple inputs, such as any application that sponsors gas, applications implementing multi-party exchanges (Griffy), etc.
Note-based solution
A standardized note ID would be used to represent the “owner” of the entire script execution. SDKs and wallets would automatically attach this note input, allowing smart contracts to easily verify the sender of external calls.
msg_sender
implementation
The msg_sender
function in the standard library would be updated to check for the standard
const NOTE_ID_SCRIPT_OWNER = 0xabcdef; // sha256("fuel-note:SCRIPT-OWNER")
pub fn caller_address() -> Result<Address, AuthError> {
// Note: `input_count()` is guaranteed to be at least 1 for any valid tx.
let inputs = input_count().as_u64();
let mut candidate = None;
let mut iter = 0;
while iter < inputs {
let type_of_input = input_type(iter);
match type_of_input {
Some(Input::Coin) => (),
Some(Input::Message) => (),
Some(Input::Note) => {
if (input_note_id(i) == NOTE_ID_SCRIPT_OWNER) {
return Ok(input_coin_owner(i).unwrap());
}
},
_ => {
// type != InputCoin or InputMessage, continue looping.
iter += 1;
continue;
}
}
// Rest of the function is unchanged
}
Per-call ownership (msg_sender
)
It may be desirable for multiple contract calls to take place within a transaction from multiple “senders”. A standardized note could allow for one account to act as the “sender” of a specific call.
For example, it might be desired to update an oracle in the same transaction as a searcher liquidates a lending market. A oracle could sign over being the “owner” of a call to the oracle update, while the searcher would be the “primary” sender for the rest of the transaction.
Predicate-based “signatures”
Problem
Currently, it’s not possible to do “signature verification” for arbitrary predicate accounts, which may implement arbitrary signature verification schemes. This means that many applications will simply use the standard EC-recover functions for signature recovery. This means that other types of wallets (EVM wallets, multisigs, passkey wallets) can not work with this type of application, making these wallets “second-class citizens” in the Fuel ecosystem.
Prior art
This same issue is faced by smart-contract wallets in the Ethereum ecosystem, which can also not directly “sign messages”. This limitation has been addressed by ERC-1271, a standard which allows smart-contract wallets to define a function and implement their own logic for validating signed messages. This standard is supported by major wallets like Safe and Argent, as well as popular applications like CoW Swap, OpenSea and 1Inch.
A similar approach is not possible with predicates, as predicates do not provide “read” functions, only the ability to approve or reject an input.
Solution
Example
Imagine a DAO voting application, similar to Snapshot or Tally. While it’s possible for users to send each vote as a transaction, it’s better for users to make a simple gasless signature, and let one account submit all signed votes in a single transaction (and pay the gas fees).
The DAO application could provide a simple scheme for “vote” notes, and various wallets could generate some form of signature over these votes. For example, a multisig could provide sufficient signatures to approve the note, and an EVM wallet could sign over the note using an EVM-style signature.
One account would generate a transaction with all these notes as inputs, as well as the respective signatures and predicate data. This single transaction could be submitted with all votes counted together.
Note that the voting smart-contract would not need to implement any cryptographic verification logic, it can simply assess all notes included.
Multisigs for predicate-based wallets
Problem
The primary way to build multi-signature wallets on Fuel is to build them as a predicate. However, these predicates must currently implement the cryptography needed to validate each required signature. This means that multisigs can not support other wallet types (such as EVM wallets or passkeys) without directly implementing that cryptographic logic. Furthermore, there is no way for one multisig to be a signer on another multisig.
Solution
A multisig wallet like Bako could provide a standard note type for “approving” transactions. This would allow one predicate-based wallet to approve transactions, and act as an individual “signer” in another predicate based wallet.
Alternatives
One alternative that has been proposed for addressing the “owner” issue is the addition of a “policy” for ownership of the transaction (see the VM PR and spec PR). However, I believe this proposal is insufficient (doesn’t address the other use-cases described in this PR, and could potentially lead to security issues).
Potential exploit
Much discussion has been had about creating more flexible predicate-based wallets, such as the demos previously created by Harsh and myself. These wallets allow users to sign over some sort of “intent”, such as individual inputs, coin amounts, and/or the script bytecode. These intents can be then be assembled into a complete transaction by a “solver”.
The danger of the policy-based solution is that an attacker can essentially “take ownership” of any account that uses this type of predicate.
Example
John (using a predicate-based wallet) deposits 10 ETH into a vault smart contract, that uses this policy-based ownership policy.
Separately, John signs an intent to swap 1 ETH for 3000 USDC, and sends this intent to an intent mempool.
A malicious solver takes this intent, and constructs the following transaction:
- 1 ETH is included as the input from John.
- 3000 USDC is included as an output to John (solving the intent).
- A policy is attached to the transaction, pointing to John’s 1 ETH input coin as the “owner” of the transaction.
- The script of the transaction withdraws the 10 ETH from the vault (which is permitted because John is the “owner”) and sends it to the attacker’s address.
It is of course possible for this type of predicate to check & validate ownership policies, preventing this issue. However this shows that using policies for ownership makes wallets insecure by default, where as using inputs makes them secure by default.