LoginSignup
4
4

[EIP7702] 将来のAA移行をスムーズに行いつつEIP3074を実現する仕組みを理解しよう!

Posted at

はじめに

初めまして。
CryptoGamesというブロックチェーンゲーム企業でエンジニアをしている cardene(かるでね) です!
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。

代表的なゲームはクリプトスペルズというブロックチェーンゲームです。

今回は、将来のアカウント抽象化への移行をスムーズにすることを視野し入れて、EIP3074と同様の機能を実現する仕組みを提案しているEIP7702についてまとめていきます!

以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。

他にも様々なEIPについてまとめています。

概要

この規格では、contract_codeフィールドと署名を追加し、署名アカウント(tx.originと同じとは限らない)をトランザクション期間中スマートコントラクトウォレットに変換する新しいトランザクションタイプを追加します。

この提案の目的は、EIP3074と同様の機能を提供することです。
EIP3074は「Auth」オペコードと「AuthCall」オペコードを導入し、EOAアカウントの操作権限を一時的にスマートコントラクト委託することができる規格です。

EIP3074については以下の記事を参考にしてください。

この新しいトランザクションタイプにより、ユーザーは通常のEOAアカウントを使用しながら、スマートコントラクトの機能を利用できるようになります。
これにより、ウォレットの機能を拡張し、より高度で柔軟な取引が可能になると期待されています。

動機

この提案の動機は、EOA(外部所有アカウント)に短期的な機能改善を加えてアプリケーションのユーザビリティを向上させ、セキュリティの改善にあります。
特に、以下の3つのアプリケーションが挙げられています。

バッチ処理

同じユーザーからの複数の操作を1つのトランザクションで実行できるようにします。
例としては、ERC20approvetransferがあげられます。
通常はapprovetransferは別々のトランザクションが必要ですが、1つのトランザクションで実行することができます。

ガススポンサーシップ

アカウントXがアカウントYに代わってトランザクションのガス代を支払います。
アカウントXはネイティブトークンの代わりにERC20トークンで支払うことも可能です。
アプリケーションの運営者がユーザーのトランザクションのガス代を負担するなどが例に挙げられます。

権限の縮小

ユーザーはサブキーに署名し、絞った権限を与えることができます。
例えば、ETHではなくERC20トークンを使用する権限や、1日あたりの総残高の1%までしか使用できない権限、特定のアプリケーションとのみやり取りできる権限などが考えられます。

EIP3074はこれらのユースケースを全て実装できますが、今後の互換性に関する懸念があります。

AUTHAUTHCALLという2つのオペコードが導入されますが、これらは「最終的なアカウント抽象化」の世界では使用されなくなります。
最終的には、すべてのユーザーがスマートコントラクトウォレットを使用する想定でいます(量子コンピュータがEOAで使用されているECDSAを破るためそうならざるを得ない)。

また、「invokerコントラクト」の仕組みが「スマートコントラクトウォレット」の仕組みとは別に発展することになり、取り組みが分断される可能性があります。

このEIPの目的は、上記2点を改善してEIP3074のすべてのユースケースを実現することです。
つまり、EOAの機能を拡張しつつ、将来のアカウント抽象化への移行をスムーズにすることを目指しています。

仕様

この提案では、FORK_BLKNUMTX_TYPEMAGICPER_CONTRACT_CODE_BASE_COSTという4つのパラメータが定義されています。
これらの具体的な値は現時点では未定(TBD)となっています。

  • FORK_BLKNUM

    • このEIPの変更が有効になるブロック番号を表します。
    • この番号のブロック以降、新しいトランザクションタイプが利用可能になります。
    • 具体的な値は、EIPの実装スケジュールや、イーサリアムネットワークの合意形成プロセスに基づいて決定されます。
  • TX_TYPE

    • 新しいトランザクションタイプを識別するための番号を表します。
    • イーサリアムでは、各トランザクションタイプに固有の番号が割り当てられています。
    • 例えば、通常のトランザクションは0x00EIP2930で導入されたアクセスリスト付きトランザクションは0x01などです。
    • 新しいトランザクションタイプには、まだ使われていない番号が割り当てられることになります。
  • MAGIC

    • contract_codeの署名を検証する時に使用される定数を表します。
    • この定数は、署名のためのメッセージハッシュを計算する時に、contract_codeと組み合わせて使用されます。
    • これにより、署名の目的が明確になり、他の用途での署名との混同を防ぐことができます。
    • 具体的な値は、衝突耐性や、他の定数との互換性を考慮して決定されます。
  • PER_CONTRACT_CODE_BASE_COST

    • contract_codeに対して課される基本コストを表します。
    • これは、contract_codeのサイズに関わらず、一律に適用されるコストです。
    • 具体的な値は、ネットワークのリソース消費と、contract_codeの実行コストのバランスを考慮して決定されます。

FORK_BLOCK_NUMBER以降、EIP2718で定義された新しいトランザクションが導入されます。
このトランザクションのTransactionTypeTX_TYPEで指定された値となります。

EIP2718については以下の記事を参考にしてください。

新しいトランザクションのTransactionPayloadは以下のようなRLP(Recursive Length Prefix)エンコーディングになります。

rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, destination, data, access_list, [[contract_code, y_parity, r, s], ...], signature_y_parity, signature_r, signature_s])

新しいトランザクションタイプでは、TransactionPayloadと呼ばれるデータ構造がRLP(Recursive Length Prefix)を用いてエンコーディングされます。

TransactionPayloadの構成要素とその説明は以下の通りです。

  • chain_id
    • トランザクションが実行されるイーサリアムネットワークのチェーンIDを表します。
    • これにより、異なるネットワーク間でのトランザクションの混同を防ぎます。
  • nonce
    • トランザクションの送信者のアカウントからこれまでに送信されたトランザクションの数を表します。
    • これは、トランザクションの重複実行を防ぐために使用されます。
  • max_priority_fee_per_gas
    • トランザクションの優先度を上げるために、マイナーに支払う追加のガス料金の上限を表します。
    • これにより、トランザクションの確認速度を調整できます。
  • max_fee_per_gas
    • トランザクションの実行に必要なガス料金の上限を表します。
    • これは、EIP1559で導入された動的な料金設定方式に基づいています。
  • gas_limit
    • トランザクションの実行に使用できる最大ガス量を表します。
    • これは、トランザクションの実行コストを制限するために使用されます。
  • destination
    • トランザクションの宛先アドレスを表します。
    • これは、トランザクションが呼び出すスマートコントラクトのアドレスや、送金先のEOAアドレスになります。
  • data
    • トランザクションに含まれる任意のデータを表します。
    • これは、スマートコントラクトの関数呼び出しや、メッセージの送信に使用されます。
  • access_list
    • トランザクションがアクセスするストレージのキーとアドレスのリストを表します。
    • これは、EIP2930で導入されたアクセスリスト機能に基づいています。
  • [[contract_code, y_parity, r, s], ...]
    • 署名済みのcontract_codeのリストを表します。
    • contract_codeは、一時的にEOAをスマートコントラクトに変換するためのコードです。
    • y_parityrsは、それぞれcontract_codeに対する署名の各部分を表します。
  • signature_y_parity, signature_r, signature_s
    • トランザクション全体に対する送信者の署名を表します。
    • これらは、トランザクションの認証と整合性を保証するために使用されます。

これらの要素がRLPでエンコーディングされ、1つのTransactionPayloadを構成します。RLPエンコーディングにより、データ構造が一意のバイト列に変換され、ネットワーク上で効率的に送信および処理されます。

新しいトランザクションタイプでは、このTransactionPayloadを使用することで、EOAに一時的にスマートコントラクトの機能を付与し、複数の操作をアトミックに実行したり、他のアカウントの代わりにトランザクションを送信したりすることができます。

RLP(Recursive Length Prefix)は、任意のネストされたバイナリデータ構造をエンコードするためのシリアライゼーション(データを適切な形式に変換する)方式です。
イーサリアムでは、トランザクションやブロックなどのデータ構造のシリアライズに広く使用されています。

RLPエンコーディングの主な特徴は以下の通りです。

  • バイト配列(バイナリデータ)とリスト(他の要素を含む配列)の2種類のデータ型をサポートしています。
  • 長さの情報を先頭に付加することで、デコード時にデータの境界を明確に識別できます。
  • 再帰的な構造をサポートしているため、リストの中にリストを含めることができます。

RLPのエンコーディングルールは以下のようになります。

バイト配列の場合

長さが1バイトで、そのバイトが0x00から0x7fの範囲の場合、そのバイト自身がRLP エンコーディングとなります。
長さが55バイト以下の場合、先頭バイトを0x80と長さの和とし、その後にバイト配列自体を連結します。
長さが55バイトより大きい場合、先頭バイトを0xb7と長さのバイト数の和とし、次に長さのバイト表現を続け、最後にバイト配列自体を連結します。

リストの場合

リストの全長が55バイト以下の場合、先頭バイトを0xc0とリストの全長の和とし、その後にリストの要素をRLPエンコーディングしたものを連結します。
リストの全長が55バイトより大きい場合、先頭バイトを0xf7とリストの全長のバイト数の和とし、次にリストの全長のバイト表現を続け、最後にリストの要素をRLPエンコーディングしたものを連結します。

このルールに従ってデータをエンコードすることで、複雑なデータ構造を一意のバイト列に変換することができます。また、エンコーディングされたデータからオリジナルのデータ構造を一意に復元することも可能です。

RLPは、シンプルで効率的なシリアライゼーション方式であるため、イーサリアムのみならず、他のブロックチェーンプロジェクトでも採用されています。

このトランザクションの固有コストは、EIP2930から継承されます。

EIP2930については以下の記事を参考にしてください。

具体的には、21000 + 16 * non-zero calldata bytes + 4 * zero calldata bytes + 1900 * access list storage key count + 2400 * access list address countとなります。
さらに、各contract_codeにつき16 * non-zero calldata bytes + 4 * zero calldata bytesのコストが追加され、PER_CONTRACT_CODE_BASE_COSTcontract_code配列の長さを掛けたコストも加算されます。

新しいトランザクションタイプの固有コストは、以下の要素から計算されます。

  • 基本コスト
    • 21000
  • 非ゼロのcalldataバイト数
    • 16ガス × 非ゼロのcalldataバイト数
  • ゼロのcalldataバイト数
    • 4ガス × ゼロのcalldataバイト数
  • アクセスリストのストレージキーの数
    • 1900ガス × ストレージキーの数
  • アクセスリストのアドレスの数
    • 2400ガス × アドレスの数
  • contract_codeの非ゼロのcalldataバイト数
    • 16ガス × 非ゼロのcalldataバイト数 × contract_codeの数
  • contract_codeのゼロのcalldataバイト数
    • 4ガス × ゼロのcalldataバイト数 × contract_codeの数
  • contract_code配列の長さに基づく追加コスト
    • PER_CONTRACT_CODE_BASE_COST × contract_code配列の長さ

これらのコストを合計したものが、新しいトランザクションタイプの実行に必要なガス代となります。

この計算式は、トランザクションのサイズやアクセスリストの内容に応じて動的にガス代を決定します。
また、contract_codeの数とサイズに基づいて追加のコストが発生するため、トランザクションの複雑さに応じてガス代が増加する仕組みになっています。

トランザクションの実行開始時に、各[contract_code, y_parity, r, s]のセットに対して以下のステップが実行されます。

  1. 署名者(signer)の特定
    • MAGICcontract_codeを連結した文字列のKeccakハッシュを計算します。
    • このハッシュ値と、y_parityrsを使って、ECDSAの署名検証を行います。
    • 署名が有効な場合、署名者のアドレス(signer)が得られます。
  2. 署名者のコントラクトコードの検証
    • 署名者のアドレスに現在設定されているコントラクトコードが空であることを確認します。
    • これは、署名者のアカウントがEOA(外部所有アカウント)であることを保証するためです。
  3. 署名者のコントラクトコードの更新
    • 署名者のアドレスのコントラクトコードを、contract_codeに指定されたコードに設定します。
    • これにより、署名者のアカウントは一時的にスマートコントラクトアカウントに変換されます。

トランザクションの実行が完了すると、各署名者のコントラクトコードが空に戻されます。
つまり、一時的に設定されたスマートコントラクトコードが削除され、署名者のアカウントはEOAに戻ります。

重要な点として、contract_codeに署名した署名者と、トランザクションを実際に送信したアカウント(tx.origin)が異なっていても問題ないとされています。
これにより、あるアカウントが他のアカウントの代わりにトランザクションを実行することが可能になります。

この一連の処理により、EOAに一時的にスマートコントラクトの機能を付与することができます。
これにより、複数の操作をアトミックに実行したり、他のアカウントの代わりにトランザクションを送信したりといった、より高度な使用例が実現できます。
同時に、トランザクション終了時にはEOAに戻されるため、アカウントの状態は適切に管理されます。

この新しいトランザクションタイプにより、EOAに短期的な機能拡張を行いつつ、将来のアカウント抽象化への移行を見据えた設計になっています。
複数の操作をアトミックに実行したり、他のアカウントの代わりにトランザクションを実行したり、権限を制限したサブキーを使用したりといった、EIP3074で目指されているユースケースを実現することができます。

補足

新しいトランザクションタイプがEIP3074のユースケースにどのように適用でき、将来のアカウント抽象化とどのように互換性を保つかについて述べています。

EIP3074のワークフローを新しい設計に変換するのは比較的簡単です。
EIP3074AUTHAUTHCALLの機能を新しいトランザクションタイプで実現するには、ユーザーウォレットのコントラクトコードを工夫する必要があります。

ユーザーウォレットのコントラクトコードは、verifyexecuteという2つの関数を持ちます。

verify関数

verify関数は、EIP3074AUTH機能の代わりとなります。
この関数では、呼び出し元のアドレス(msg.sender)に対して権限を付与します。
具体的には、TSTORE(トランザクションストレージ)を使って、authorized[msg.sender, ...] = Trueというデータを保存します。
これにより、呼び出し元のアドレスが特定の操作を実行する権限を持つことが記録されます。

execute関数

execute関数は、EIP3074AUTHCALL機能の代わりとなります。
この関数では、まずTLOAD(トランザクションストレージ読み込み)を使って、authorized[msg.sender, ...]のデータを確認します。
呼び出し元のアドレスが権限を持っている場合、execute関数内の処理が実行されます。
権限がない場合は、実行が拒否されます。

これらの関数を使うことで、EIP3074AUTHAUTHCALLの機能を新しいトランザクションタイプで実現できます。

ユーザーウォレットのコントラクトコードは、DELEGATECALLフォワーダーを使ってガス代を節約することもできます。DELEGATECALLは、別のコントラクトのコードを呼び出し元のコントラクトのコンテキストで実行する特殊な呼び出し方法です。これにより、コードの重複を避けてガス代を削減できます。

以上のように、EIP-3074のワークフローを新しいトランザクションタイプに変換するのは比較的簡単です。ユーザーウォレットのコントラクトコードにverifyexecuteの関数を実装し、TSTORETLOADを使って権限管理を行うだけで、EIP-3074と同等の機能を実現できます。

将来のアカウント抽象化との前方互換性については、このEIPはERC4337RIP7560に依存することなく、最終的なアカウント抽象化と非常に互換性の高い設計になっています。

ERC4337については以下の記事を参考にしてください。

https://qiita.com/cardene/items/8c97ca7c93c4557ebfc2
https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58/HaPTTeXZeXRG3K5JfXesmKet9eKTbcFUixW6IaMvt4c

具体的には以下のような点が挙げられます。

ユーザーが署名する必要があるコントラクトコードは、文字通り既存のERC4337ウォレットコードになる可能性があります。これにより、既存のウォレットの実装をそのまま活用できます。

使用されるコードパス(ログラムの実行において、特定の条件に基づいて選択される一連の命令)は、将来のアカウント抽象化においても意味を持ち続けると期待されています。

アカウント抽象化とは、EOAとコントラクトアカウントの区別をなくし、すべてのアカウントをスマートコントラクトで表現するという概念です。
将来的には、イーサリアムのすべてのアカウントがスマートコントラクトウォレットに置き換えられる可能性があります。

この提案では、新しいトランザクションタイプで使用されるコードパスが、完全にスマートコントラクトウォレットベースのアカウントシステムにおいても有効であるように設計されています。
つまり、今回の変更で導入されるロジックは、アカウント抽象化が実現された後も、そのまま活用できる可能性が高いです。

例えば、verifyexecuteといった関数は、権限管理や実行制御のための一般的な仕組みであり、スマートコントラクトウォレットにおいても同様の機能が必要になると考えられます。
また、TSTORETLOADのようなトランザクションストレージの操作も、アカウント抽象化後のシステムで活用できる可能性があります。

このように、今回の提案で使用されるコードパスは、将来のアカウント抽象化を見据えて設計されており、移行後も互換性を保つことができると期待されています。
これにより、イーサリアムのアカウントシステムの段階的な進化を実現しつつ、開発者や利用者にとってスムーズな移行が可能になります。

EOAベースのシステムとAAベースのシステムが全く異なるコードベースを持ち、互換性がない状態になると問題です。
これが起こると、開発者はそれぞれのシステム向けに別々のツールやライブラリを用意する必要があり、効率的ではありません。

しかし、この提案では、新しいトランザクションタイプを導入する時に、将来のAAシステムとの互換性を考慮しています。
具体的には、verifyexecuteのような関数、TSTORETLOADのようなトランザクションストレージの操作など、AAシステムでも活用できる可能性が高い仕組みを取り入れています。

これにより、EOAベースのシステムとAAベースのシステムが、ある程度同じコードエコシステムを共有できるようになります。
つまり、開発者は同じツールやライブラリを使って、両方のシステムに対応するスマートコントラクトを開発できるようになるのです。

ただし、すべてのワークフローがシームレスに移行できるわけではありません。
一部のワークフローでは、AAシステムに完全に適合させるために工夫が必要となるでしょう。
AAシステムでは、すべてのアカウントがスマートコントラクトベースになるため、EOAベースのシステムとは異なるロジックが必要になる場面もあります。

しかし、そのようなワークフローは全体から見れば比較的小さなサブセットであると考えられています。
大部分のワークフローは、この提案で導入される仕組みを活用することで、EOAベースのシステムからAAベースのシステムへとスムーズに移行できるはずです。

もし新しいトランザクションタイプを導入する時に、EOA特有の機能に依存したオペコード(EVM命令)を追加してしまうと、EOA後の世界では、それらのオペコードが役に立たなくなってしまいます。

そうなると、EOA後の世界へのスムーズな移行が難しくなります。
新しいトランザクションタイプを導入した後に、再びオペコードを変更する必要が出てくるかもしれません。
しかし、この提案では、そのような問題を回避するために、現在のEVM(イーサリアム仮想マシン)で実装可能な範囲内で、新しい機能を導入しています。
EOA特有の機能に依存せず、スマートコントラクトベースのアカウントにも適用できる一般的な仕組みを取り入れているのです。
例えば、verifyexecuteのような関数、TSTORETLOADのようなトランザクションストレージの操作は、EOAベースのシステムでもスマートコントラクトベースのシステムでも活用できる可能性が高いです。

EOAが一時的に自分自身をコントラクトに変換してERC4337バンドルに含めることができ、既存のEntryPointとの互換性を保ちます。これにより、アカウント抽象化の枠組みにスムーズに統合できます。
これが実装されれば、トランザクションの最後にコントラクトコードを空に戻さないようにフラグを追加するだけで、EIP5003を実現できます。。

この提案はEIP3074の機能を実現しつつ、将来のアカウント抽象化への移行も見据えた設計になっています。
既存のワークフローを活用しながら、スマートコントラクトウォレットのエコシステムとの互換性も確保しています。
これにより、イーサリアムのアカウントモデルの段階的な発展を促進することができると期待されています。

互換性

このEIPは、「アカウントの残高は、そのアカウントから発行されたトランザクションによってのみ減少する」というルールを破る提案です。
これは、Mempoolの設計や、インクルージョンリストなどの他のEIPに影響を与えます。
ただし、これらの問題は、EIP3074を含む、同様の機能を提供する他の提案にも共通するものです。

ルールの破棄

現在のイーサリアムでは、アカウントの残高は、そのアカウントが発信したトランザクションによってのみ減少します。
しかし、このEIPでは、あるアカウントが他のアカウントの代わりにトランザクションを実行できるようになります。
つまり、アカウントAがアカウントBの代わりにトランザクションを実行した場合、アカウントBの残高が、アカウントBが直接関与しないトランザクションによって減少する可能性があります。
これは、「アカウントの残高はそのアカウントが発信したトランザクションでのみ減少する」というルールを破ることになります。

Mempoolへの影響

Mempoolは、マイナーやバリデーターがトランザクションを受信し、ブロックに取り込むまでの一時的な保管場所です。
現在のMempool設計は、アカウントの残高に関するルールを前提としています。
しかし、このEIPが導入されると、アカウントの残高が予期せず変更される可能性があるため、Mempoolの管理がより複雑になります。
Mempoolは、トランザクションの依存関係や実行可能性を適切に処理できるように、設計を見直す必要があるかもしれません。

他のEIPへの影響

インクルージョンリストは、あるアカウントが別のアカウントからのトランザクションを受け入れるかどうかを指定するためのメカニズムです。
現在のインクルージョンリストの設計は、アカウントの残高に関するルールを前提としています。
このEIPが導入されると、インクルージョンリストに記載されていないアカウントが、別のアカウントの代わりにトランザクションを実行し、残高を変更する可能性があります。
したがって、インクルージョンリストの設計も見直しが必要になるかもしれません。

他の提案との共通点

EIP3074など、アカウントの代わりにトランザクションを実行する機能を提供する他の提案も、同様の問題に直面します。
これらの提案は、いずれもアカウントの残高に関するルールを破るため、Mempoolやインクルージョンリストなどへの影響は避けられません。
したがって、これらの問題に対処するためには、イーサリアムプロトコル全体を見渡した調整が必要になります。

セキュリティ

EIP3074と同様に、このEIPにもセキュリティ上の考慮事項があり、特にユーザーのウォレットは、どのcontract_codeに署名するかについて取り上げられています。

EIP3074との共通点

EIP3074は、AUTHAUTHCALLという2つの新しいオペコードを導入することで、EOAの操作権限を一時的にコントラクトに渡すことを目的としています。
このEIPも、新しいトランザクションタイプを導入することで、同様の機能を提供しようとしています。
したがって、両者には共通のセキュリティ上の考慮事項があります。

contract_codeへの署名

このEIPでは、ユーザーのウォレットはcontract_codeに署名することで、そのコードを自分のアカウントに関連付けます。
contract_codeは、ユーザーのアカウントが一時的にスマートコントラクトになる時に実行されるコードです。
つまり、contract_codeに署名することは、そのコードにユーザーのアカウントを操作する権限を与えることになります。

悪意のあるcontract_codeのリスク

もし、ユーザーが悪意のあるcontract_codeに署名してしまうと、そのコードがユーザーのアカウントを不正に操作する可能性があります。
例えば、悪意のあるcontract_codeは、ユーザーの資金を盗んだり、不正な取引を行ったりすることができます。
したがって、ユーザーのウォレットは、contract_codeの内容を十分に理解し、信頼できるソースからのコードにのみ署名すべきです。

contract_codeの検証

ユーザーのウォレットは、contract_codeに署名する前に、そのコードを徹底的に検証する必要があります。
検証では、contract_codeの動作を詳細に分析し、潜在的なセキュリティリスクを特定すべきです。
また、contract_codeが、ユーザーの意図した目的にのみ使用されるように、適切な制限が設けられていることを確認すべきです。

信頼できるソースの重要性

ユーザーのウォレットは、信頼できるソースからcontract_codeを取得することが重要です。
信頼できるソースとは、セキュリティ監査を受けており、コミュニティから広く認知されているスマートコントラクトライブラリやツールなどです。
未知のソースや評判の低いソースからのcontract_codeは、セキュリティリスクが高い可能性があるため、避けるべきです。

このEIPでは、ユーザーのウォレットがcontract_codeに署名することで、そのコードにアカウントを操作する権限を与えます。
したがって、ユーザーのウォレットは、contract_codeの内容を十分に理解し、信頼できるソースからのコードにのみ署名することが非常に重要です。

最後に

今回は「将来のアカウント抽象化への移行をスムーズにすることを視野し入れて、EIP3074と同様の機能を実現する仕組みを提案しているEIP7702」についてまとめてきました!
いかがだったでしょうか?

質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!

Twitter @cardene777

他の媒体でも情報発信しているのでぜひ他も見ていってください!

4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4