FuelLabs / sway

🌴 Empowering everyone to build reliable and efficient smart contracts.
https://docs.fuel.network/docs/sway/
Apache License 2.0
62.8k stars 5.35k forks source link

Standard Ethereum signing support modules (for MetaMask, Ledger, Trezor and WalletConnect) #1065

Open SilentCicero opened 2 years ago

SilentCicero commented 2 years ago

Background

Fuel can support Ethereum signing schemes (commonly EIP-712 and the eth_sign RPC endpoint) via it's keccak256 and ecrecover opcode in the FuelVM in combination with either a simple predicate script or basic proxy contract. This would allow for easy UX adoption from users / projects who use common Ethereum wallets / apps such as MetaMask, Ledger, Trezor and WalletConnect without the need for additional installation, apps, plugins or special key management.

We should provide an easily accessible set of modules / tools for Sway developers to leverage pre-built secure modules for this flow.


Predicate Pseudo Code

predicate;

use std::address::*;
use std::metadata::witness;
use std::hash::keccak256;
use std::serialize::serialize_packed;

const owner = Address(0);

fn main() -> bool {
    let digest = hash(
        serialize_packed("Ethereum signed message/", tx_id()
    ), HashMethod::keccak256);

    owner == ec_recover(digest, witness(0))
}

Contract Pseudo Code

contract;

use std::address::*;
use std::metadata::witness;
use std::hash::keccak256;
use std::serialize::serialize_packed;

////////////////////////////////////////
// ABI declarations
////////////////////////////////////////

abi EthereumSignableWallet {
    fn forward(destination: Address, data: Bytes);
}

////////////////////////////////////////
// Storage declarations
////////////////////////////////////////

const owner = Address(0); // this would be filled in at deployment.

////////////////////////////////////////
// Helper methods
////////////////////////////////////////

fn recover_ethereum_witness() -> Address {
    let digest = keccak256(
        serialize_packed("Ethereum signed message/", tx_id()
    );

    ec_recover(digest, witness(0))
}

////////////////////////////////////////
// ABI definitions
////////////////////////////////////////

impl EthereumSignableWallet for Contract {
    fn forward(destination: Address, data: Bytes) {
        assert(owner == recover_ethereum_witness());
        call(destination, data)
    }
}

Users can simply use their Ethereum wallets in our system, but will be required to either put their spendable deposits behind a restrictive predicate or put all their assets into a proxy contract.

UX Flow Predicate Example

0) User has 1 Dai on Ethereum 1) Deposits 1 Dai into Fuel Bridge from Ethereum [asset it deposited to a predicate hash not their own address] 2) When spending 1 Dai in Fuel, user provides spendable deterministic predicate and necessary eth_sign signature to unlock assets.

UX Flow Contract Proxy Example

0) User has 1 Dai on Ethereum 1) User or service creates simple stateless EthereumSignableProxy proxy contract on Fuel 2) User deposits 1 Dai into Fuel Bridge from Ethereum [asset is deposited to proxy contract address] 3) When spending 1 Dai in Fuel, user uses the proxy contract in Fuel to call other contracts and spend their assets by providing eth_sign signature to unlock contract.

UX Flow Predicate + Contract Design (i.e. the Uniswap Flow)

0) User has 1 Dai on Ethereum 1) Deposits 1 Dai into Fuel Bridge from Ethereum [asset it deposited to a predicate hash not their own address] 2) User attempts to use App X, prompted with message, "In order to use App X you must have a proxy contract Y.. Deploy Now" 3) User creates simple stateless EthereumSignableProxy proxy contract on Fuel using some of the Dai to pay for deployment 4) User then uses App X

For most users, the contract is likely the best route, however, if we want a flow where the User pays for everything and there is no service in between we must do the third UX flow using both the predicate and smart-contract. Of course, that flow is the most complex.

This is very simpler to the use of DS-Proxy in many Uniswap or Dex Designs.

adlerjohn commented 2 years ago

To provide some more context: Metamask doesn't nicely support anything other than signing Ethereum transactions and EIP-191 messages, which unfortunately require the non-standard keccak256 hash function and the hard-coded but not well-specified domain separation string "\x19Ethereum Signed Message:\n" + len(message). We don't want to enshrine either into the Fuel v2 protocol.

We can get around this limitation by, instead of using EOA-owned coins, using predicate-owned coins. The predicate would simply unlock the UTXO if an appropriate Metamask-originated signature is provided. This pushed the hash function and domain separator outside of consensus and into the application level.