J'ai récemment écrit un article sur Karma Money , un système monétaire alternatif basé sur un jeton ERC-20 unique. J'ai envisagé Karma Money comme un système fermé, dans lequel les utilisateurs peuvent également payer des frais de transaction avec du karma. Construire notre propre blockchain serait un moyen d’en faire une réalité, mais c’est une tâche difficile. Pour assurer la sécurité et la crédibilité de la blockchain, il faudrait mettre en place l’infrastructure nécessaire et une communauté suffisamment grande. Il serait bien plus simple d’utiliser une blockchain existante. Il existe des chaînes telles que Gnosis ou Polygon , qui sont entièrement compatibles avec Ethereum et ont des frais de transaction très faibles. Les frais pour une transaction ERC20 sur ces chaînes sont généralement inférieurs à 1 centime. Le problème est que ces frais doivent être payés dans la propre crypto-monnaie de la chaîne, ce qui peut compliquer l'utilisation de la chaîne pour les utilisateurs. Heureusement, il existe une solution de transition, les transactions métalliques EIP-712 .
Dans le cas d'une métatransaction, l'utilisateur crée une structure décrivant une transaction, puis la signe numériquement avec sa clé privée. C’est comme faire un chèque pour quelqu’un. La transaction signée numériquement est ensuite envoyée à un nœud relais, qui la soumet à un contrat intelligent. Le contrat vérifie la signature et, si elle est valide, exécute la transaction. Le nœud relais paie l'exécution du contrat.
Dans une transaction karma, par exemple, l'utilisateur fournit le montant de la transaction (par exemple, 10 dollars karma), l'adresse Ethereum à laquelle il souhaite envoyer le montant et les frais de transaction (en dollars karma) qu'il est prêt à offrir. pour la transaction. Cette structure est signée numériquement et envoyée à un nœud relais. Si le nœud trouve les frais de transaction acceptables, il soumet la structure signée numériquement au contrat karma, qui vérifie la signature et exécute la transaction. Étant donné que les frais de transaction sont payés dans la devise native de la blockchain par le nœud relais, il apparaît à l'utilisateur comme s'il payait la transaction avec des dollars karma sans avoir besoin de sa propre blockchain.
Après la théorie, revenons à la pratique.
La norme EIP-712 définit comment signer des paquets de données structurées de manière standardisée. MetaMask affiche ces données structurées dans un format lisible pour l'utilisateur. Une structure conforme à EIP-712, telle qu'affichée sur MetaMask ( peut être testée sur cette URL ) ressemble à ceci :
La transaction ci-dessus a été générée à l'aide du code simple suivant :
async function main() { if (!window.ethereum || !window.ethereum.isMetaMask) { console.log("Please install MetaMask") return } const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' }); const chainId = await window.ethereum.request({ method: 'eth_chainId' }); const eip712domain_type_definition = { "EIP712Domain": [ { "name": "name", "type": "string" }, { "name": "version", "type": "string" }, { "name": "chainId", "type": "uint256" }, { "name": "verifyingContract", "type": "address" } ] } const karma_request_domain = { "name": "Karma Request", "version": "1", "chainId": chainId, "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" } document.getElementById('transfer_request')?.addEventListener("click", async function () { const transfer_request = { "types": { ...eip712domain_type_definition, "TransferRequest": [ { "name": "to", "type": "address" }, { "name": "amount", "type": "uint256" } ] }, "primaryType": "TransferRequest", "domain": karma_request_domain, "message": { "to": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", "amount": 1234 } } let signature = await window.ethereum.request({ "method": "eth_signTypedData_v4", "params": [ accounts[0], transfer_request ] }) alert("Signature: " + signature) }) } main()
L' eip712domain_type_definition est une description d'une structure générale qui contient les métadonnées. Le champ name est le nom de la structure, le champ version est la version de définition de la structure, et les champs chainId et verifyContract déterminent à quel contrat le message est destiné. Le contrat d'exécution vérifie ces métadonnées afin de s'assurer que la transaction signée n'est exécutée que sur le contrat cible.
Le karma_request_domain contient la valeur spécifique des métadonnées définies par la structure EIP712Domain.
La structure réelle que nous envoyons à MetaMask pour signature est contenue dans la variable transfer_request . Le bloc types contient les définitions de types. Ici, le premier élément est la définition obligatoire EIP712Domain, qui décrit les métadonnées. Ceci est suivi par la définition réelle de la structure, qui dans ce cas est la TransferRequest. C'est la structure qui apparaîtra dans MetaMask pour l'utilisateur. Le bloc de domaine contient la valeur spécifique des métadonnées, tandis que le message contient la structure spécifique que nous souhaitons signer avec l'utilisateur.
En ce qui concerne l'argent karma, un exemple de la façon dont une métatransaction est organisée et envoyée au contrat intelligent ressemble à ceci :
const types = { "TransferRequest": [ { "name": "from", "type": "address" }, { "name": "to", "type": "address" }, { "name": "amount", "type": "uint256" }, { "name": "fee", "type": "uint256" }, { "name": "nonce", "type": "uint256" } ] } let nonce = await contract.connect(MINER).getNonce(ALICE.address) const message = { "from": ALICE.address, "to": JOHN.address, "amount": 10, "fee": 1, "nonce": nonce } const signature = await ALICE.signTypedData(karma_request_domain, types, message) await contract.connect(MINER).metaTransfer(ALICE.address, JOHN.address, 10, 1, nonce, signature) assert.equal(await contract.balanceOf(ALICE.address), ethers.toBigInt(11))
La variable types définit la structure de la transaction. Le « de » est l'adresse de l'expéditeur, tandis que le « à » est l'adresse du destinataire. Le montant représente la quantité de tokens à transférer. Les frais sont le « montant » de tokens que nous offrons au nœud relais en échange de l’exécution de notre transaction et couvrant le coût dans la devise native de la chaîne. Le « nonce » sert de compteur pour garantir l’unicité de la transaction. Sans ce champ, une transaction pourrait être exécutée plusieurs fois. Cependant, grâce au nonce, une transaction signée ne peut être exécutée qu'une seule fois.
La fonction signTypedData fournie par ethers.js facilite la signature des structures EIP-712. Il fait la même chose que le code présenté précédemment mais avec une utilisation plus simple.
Le métaTransfer est la méthode du contrat karma pour exécuter la méta-transaction. Voyons voir comment ça fonctionne:
function metaTransfer( address from, address to, uint256 amount, uint256 fee, uint256 nonce, bytes calldata signature ) public virtual returns (bool) { uint256 currentNonce = _useNonce(from, nonce); (address recoveredAddress, ECDSA.RecoverError err) = ECDSA.tryRecover( _hashTypedDataV4( keccak256( abi.encode( TRANSFER_REQUEST_TYPEHASH, from, to, amount, fee, currentNonce ) ) ), signature ); require( err == ECDSA.RecoverError.NoError && recoveredAddress == from, "Signature error" ); _transfer(recoveredAddress, to, amount); _transfer(recoveredAddress, msg.sender, fee); return true; }
Afin de valider la signature, il faut d'abord générer le hash de la structure. Les étapes exactes pour ce faire sont décrites en détail dans la norme EIP-712 , qui comprend un exemple de contrat intelligent et un exemple de code javascript .
En résumé, l'essentiel est que nous combinons le TYPEHASH (qui est le hachage de la description de la structure) avec les champs de la structure en utilisant abi.encode. Produit ensuite un hachage keccak256. Le hachage est transmis à la méthode _hashTypedDataV4, héritée du contrat EIP712 OpenZeppelin dans le contrat Karma. Cette fonction ajoute des métadonnées à notre structure et génère le hachage final, rendant la validation de la structure très simple et transparente. La fonction la plus externe est ECDSA.tryRecover, qui tente de récupérer l'adresse du signataire à partir du hachage et de la signature. Si elle correspond à l'adresse du paramètre « from », la signature est valide. A la fin du code, la transaction proprement dite est exécutée et le nœud relais effectuant la transaction reçoit les frais.
EIP-712 est une norme générale pour les structures de signature, ce qui en fait l'une des nombreuses utilisations pour la mise en œuvre de méta-transactions. Comme la signature peut être validée non seulement avec des contrats intelligents, elle peut également être très utile dans des applications non blockchain. Par exemple, il peut être utilisé pour l'authentification côté serveur, où l'utilisateur s'identifie avec sa clé privée. Un tel système peut fournir un niveau élevé de sécurité généralement associé aux crypto-monnaies, permettant d'utiliser une application Web uniquement avec une clé matérielle. De plus, des appels API individuels peuvent également être signés à l'aide de MetaMask.
J'espère que ce bref aperçu de la norme EIP-712 a été une source d'inspiration pour beaucoup et que vous pourrez l'utiliser dans des projets basés sur la blockchain et non-blockchain.
Chaque code est disponible sur le dépôt GitHub de karma money .