티스토리 뷰

Github

 

GitHub - piatoss3612/aa-demo

Contribute to piatoss3612/aa-demo development by creating an account on GitHub.

github.com


Demo

 

AA Demo

 

aa-demo-gules.vercel.app


준비물

  • Privy : 소셜 로그인 및 임베디드 지갑 제공자, 대시보드 로그인 및 앱 생성하기
  • Biconomy : 계정 추상화 툴킷 제공자, 대시보드 로그인 및 Paymaster 생성하기
  • nvm, node, yarn (또는 npm) : 웹 앱 개발환경
  • foundry : 이더리움 스마트 컨트랙트 개발환경

Foundry 프로젝트

프로젝트 생성

$ forge init contracts

OpenZeppelin 라이브러리 설치

$ forge install OpenZeppelin/openzeppelin-contracts --no-commit

ERC20Airdrop 컨트랙트 생성

 src 디렉터리 안에 ERC20Airdrop.sol 파일을 생성하고 붙여 넣기

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract ERC20Airdrop is ERC20 {
    error AirdropAlreadyRecieved();

    uint256 public constant AIRDROP_AMOUNT = 100 * 10 ** 18;

    event Airdrop(address indexed to, uint256 amount);

    mapping(address => bool) public airdropRecieved;

    constructor() ERC20("Airdrop Token", "ADT") {}

    function airdrop() public {
        address sender = _msgSender();

        if (airdropRecieved[sender]) {
            revert AirdropAlreadyRecieved();
        }

        airdropRecieved[sender] = true;
        _mint(sender, AIRDROP_AMOUNT);

        emit Airdrop(sender, AIRDROP_AMOUNT);
    }
}

ERC20Airdrop 컨트랙트 배포 스크립트 작성

 script 디렉터리 안에 ERC20Airdrop.s.sol 파일을 생성하고 붙여 넣기

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {Script, console} from "forge-std/Script.sol";
import {ERC20Airdrop} from "../src/ERC20Airdrop.sol";

contract ERC20AirdropScript is Script {
    ERC20Airdrop public airdrop;

    function run() public {
        vm.broadcast();
        airdrop = new ERC20Airdrop();

        console.log("Airdrop token deployed at address: ", address(airdrop));
    }
}

foundry.toml 파일에 rpc endpoint 추가

 sepolia 테스트 네트워크의 rpc url을 사용

[rpc_endpoints]
sepolia = "https://ethereum-sepolia-rpc.publicnode.com"

컨트랙트 배포

 --account 플래그를 사용하는 방법 참고: https://piatoss3612.tistory.com/139

 

[Foundry] .env 파일 사용하지 마세요

프라이빗 키를 .env 파일에 저장하는 것은 안전하지 않다 제목 어그로를 좀 끌어봤는데 사실 .env 파일 사용하지 마라는 것은 아닙니다. .env 파일 좋다 이겁니다. 다만 프라이빗 키를 .env 파일에 저

piatoss3612.tistory.com

 또는 --private-key 플래그를 사용하여 프라이빗 키를 직접 사용하는 방법도 있다.

$ forge script script/ERC20Airdrop.s.sol --rpc-url sepolia --account piatoss --sender 0x965b0e63e00e7805569ee3b428cf96330dfc57ef --broadcast -vvvv
...
== Logs ==
  Airdrop token deployed at address:  0xf7867F6C6E1d3a77F00F911a47b5842ff3fc4516

Next.js 프로젝트

프로젝트 생성

$ yarn create next-app web

의존성 설치

Chakra UI 컴포넌트 라이브러리

yarn add @chakra-ui/react @emotion/react @emotion/styled framer-motion

Privy 및 Biconomy 라이브러리

yarn add @privy-io/react-auth @biconomy/account @biconomy/bundler @biconomy/common @biconomy/paymaster

그 외 라이브러리

yarn add @tanstack/react-query viem

.env.local 파일 생성

NEXT_PUBLIC_PRIVY_APP_ID=
NEXT_PUBLIC_BICONOMY_BUNDLER_URL=
NEXT_PUBLIC_BICONOMY_PAYMASTER_API_KEY=

Privy App ID

 대시보드에서 애플리케이션을 생성한 뒤, API Keys 탭을 확인

Privy App ID

Biconomy Bundler URL

 대시보드에서 Bundlers 선택, 왼쪽의 Testnet Bundler 주소를 복사하여 붙여 넣고 chain id만 변경. sepolia의 chain id는 11155111

Biconomy Bundler URL

Biconomy Paymaster API Key

 대시보드에서 Paymaster를 생성하고 Overview 왼쪽 박스의 API Key 확인.

Biconomy Paymaster API Key

 여기서 Gas-Tank 탭으로 들어가 가스비 대납에 사용할 이더를 소량 넣어놓아야 함.

Deposit ETH

PrivyProvider 설정

  •  embeddedWallets : 지갑 로그인이 아닌 방식을 통해 로그인한 사용자들의 임베디드 지갑을 로그인 시에 자동으로 생성
<PrivyProvider
  appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID || ""}
  config={{
    appearance: {
      theme: "light",
      accentColor: "#676FFF",
      logo: "https://your-logo-url",
    },
    // Create embedded wallets for users who don't have a wallet
    embeddedWallets: {
      createOnLogin: "users-without-wallets",
    },
  }}
></PrivyProvider>

BiconomyProvider 설정

  1. 사용자가 최초 로그인 시에 임베디드 지갑이 생성
  2. 지갑이 생성되면 useEffect에서 사용자의 Biconomy 스마트 계정(계정 추상화)을 생성
import {
  BiconomySmartAccountV2,
  DEFAULT_ECDSA_OWNERSHIP_MODULE,
  DEFAULT_ENTRYPOINT_ADDRESS,
  createECDSAOwnershipValidationModule,
  createSmartAccountClient,
} from "@biconomy/account";
import { Bundler, IBundler } from "@biconomy/bundler";
import { BiconomyPaymaster, IPaymaster } from "@biconomy/paymaster";
import { ConnectedWallet, usePrivy, useWallets } from "@privy-io/react-auth";
import { createContext, useEffect, useMemo, useState } from "react";
import { sepolia } from "viem/chains";

interface BiconomyContextProps {
  smartAccount?: BiconomySmartAccountV2;
  smartAccountAddress?: string;
}

const BiconomyContext = createContext<BiconomyContextProps>({
  smartAccount: undefined,
  smartAccountAddress: undefined,
});

const BiconomyProvider = ({ children }: { children: React.ReactNode }) => {
  const [smartAccount, setSmartAccount] = useState<
    BiconomySmartAccountV2 | undefined
  >(undefined);
  const [smartAccountAddress, setSmartAccountAddress] = useState<
    string | undefined
  >(undefined);
  const { wallets } = useWallets();
  const { ready, authenticated } = usePrivy();

  const bundler: IBundler = useMemo(
    () =>
      new Bundler({
        bundlerUrl: process.env.NEXT_PUBLIC_BICONOMY_BUNDLER_URL || "",
        chainId: sepolia.id,
        entryPointAddress: DEFAULT_ENTRYPOINT_ADDRESS,
      }),
    []
  );

  const paymaster: IPaymaster = useMemo(
    () =>
      new BiconomyPaymaster({
        paymasterUrl: process.env.NEXT_PUBLIC_BICONOMY_PAYMASTER_URL || "",
      }),
    []
  );

  const createBiconomyAccountFromEOA = async (wallet: ConnectedWallet) => {
    try {
      await wallet.switchChain(sepolia.id);
      const provider = await wallet.getEthersProvider();
      const signer = provider.getSigner();

      const validationModule = await createECDSAOwnershipValidationModule({
        signer: signer,
        moduleAddress: DEFAULT_ECDSA_OWNERSHIP_MODULE,
      });

      const biconomySmartAccount = await createSmartAccountClient({
        signer: signer,
        bundlerUrl: process.env.NEXT_PUBLIC_BICONOMY_BUNDLER_URL || "",
        biconomyPaymasterApiKey:
          process.env.NEXT_PUBLIC_BICONOMY_PAYMASTER_API_KEY || "",
        chainId: sepolia.id,
        entryPointAddress: DEFAULT_ENTRYPOINT_ADDRESS,
        defaultValidationModule: validationModule,
        rpcUrl: "https://ethereum-sepolia-rpc.publicnode.com",
      });

      const address = await biconomySmartAccount.getAddress();

      setSmartAccount(biconomySmartAccount);
      setSmartAccountAddress(address);
    } catch (error) {
      console.error(error);
    }
  };

  useEffect(() => {
    if (ready && authenticated) {
      const embeddedWallet = wallets.find(
        (wallet) => wallet.walletClientType === "privy"
      );
      if (embeddedWallet && !smartAccount) {
        createBiconomyAccountFromEOA(embeddedWallet);
      }
    }
  }, [wallets]);

  return (
    <BiconomyContext.Provider
      value={{
        smartAccount,
        smartAccountAddress,
      }}
    >
      {children}
    </BiconomyContext.Provider>
  );
};

export { BiconomyContext, BiconomyProvider };

Airdrop 실행

Airdrop 버튼을 누르면 

const handleAirdrop = useCallback(async () => {
  if (!smartAccount || !smartAccountAddress) {
    toast({
      title: "Smart Account not found",
      status: "error",
      duration: 3000,
      isClosable: true,
    });
  }

  try {
    setIsLoading(true);

    const data = encodeFunctionData({
      abi: ERC20AirdropABI,
      functionName: "airdrop",
    });

    const tx = {
      to: ERC20AirdropAddress,
      data,
    };

    const airdropOpResponse = await smartAccount?.sendTransaction(tx, {
      paymasterServiceData: { mode: PaymasterMode.SPONSORED },
    });

    const { transactionHash } = await airdropOpResponse?.waitForTxHash()!;

    if (!transactionHash) {
      throw new Error("Transaction hash not found");
    }

    toast({
      title: "Airdropping tokens",
      description: `https://sepolia.etherscan.io/tx/${transactionHash}`,
      status: "success",
      duration: 3000,
      isClosable: true,
    });
  } catch (error) {
    console.error(error);
    toast({
      title: "Error airdropping tokens",

      status: "error",
      duration: 3000,
      isClosable: true,
    });
  } finally {
    setIsLoading(false);
  }
}, [smartAccount, smartAccountAddress]);

데모

소셜 로그인

소셜 로그인

 로그인이 성공하면 아래와 같이 Privy 사용자 스냅샷이 업데이트되고 임베디드 지갑이 자동으로 생성된 것을 확인 가능.

에어드롭받기

에어드롭받기

 트랜잭션이 처리될 때 스마트 계정이 아직 배포되지 않은 관계로, 먼저 스마트 계정이 생성된다.

스마트 계정 생성

 그러고 나서 에어드롭이 실행된다.

토큰 전송

 그렇다면 이 모든 과정은 도대체 어떻게 진행되는 것일까?


동작 과정 이해하기

Off-chain

  1. Dapp에서 UserOp 요청을 생성, Biconomy Paymaster Service로 전달. (이때 Paymaster MODE는 'SPONSORED')
  2. Paymaster Service에서 요청을 검사(화이트리스트, 요청 제한, 잔액 확인 등)를 거쳐 적절한 paymasterAndData를 생성하여 반환.
  3. 반환된 paymasterAndData는 userOp에 포함되어 서명자(임베디드 지갑 - Privy)에 의해 서명되고 번들러에게 전달된다.

On-chain

  1. 번들러는 요청을 off-chain에서 시뮬레이션하고 검증하며, 여러 요청들을 하나로 묶어 handleOps 트랜잭션을 생성하여 on-chain 상의 EntryPoint 컨트랙트에게 전달한다.
  2. EntryPoint 컨트랙트는 각 userOp를 사용자가 지정한 스마트 계정의 맥락에서 검증하는 validateOp 함수를 실행한다.
  3. userOp에 paymasterAndData 필드가 포함되어 있는 경우, Sponsorship Paymaster 컨트랙트에서  validatePaymasterOp 함수를 실행한다. 검증이 성공하면 Paymaster 컨트랙트는 자신이 가지고 있는 이더를 사용해 EntryPoint가 가스비를 충당하도록 한다.
  4. userOp가 검증되었고 가스비도 충당되었다면, EntryPoint는 스마트 계정의  executeOp 함수를 실행한다.
  5. 스마트 계정은 userOp를 실행한다.
  6. 마지막으로, EntryPoint 컨트랙트는 Paymaster 컨트랙트에서 postOp 함수를 실행하여 Paymaster의 보충작업이 필요한 경우를 상정하고 실행한다.

계정 추상화의 장단점

장점

더 나은 사용자 경험

  • 별도의 지갑 생성 절차 없이 소셜 로그인을 통해 자동으로 임베디드 지갑 생성
  • 니모닉이나 프라이빗 키를 별도로 보관하고 관리할 필요가 없음
  • 트랜잭션이 무엇이고 가스비는 무엇이고 이런 복잡한 내용을 몰라도 됨
  • 가스비에 대한 부담 감소 (Paymaster를 적용한 경우)

단점

프로젝트 관리자의 비용적 부담

  • 스마트 계정 배포 비용 & 가스비에 대한 부담 (Paymaster를 적용한 경우)
  • 서드파티(소셜 로그인 제공자 & 계정 추상화 툴킷 제공자)에 대한 의존성과 비용

계정 추상화 자체에 대한 이해가 어려움

탈중앙화되어 있지 않고 기업형 관리자가 존재


결론

  계정 추상화는 빠르고 간편하게 새로운 사용자를 Web3 세계로 온보딩할 수 있으므로 매스 어돕션, 게임 체인저가 될 수 있다 -> 뭔가 아직은 이해가 잘 안돼서 그런지 호들갑 같기도 한데, 직접 사용해보니 확실히 사용자 경험의 관점에서 진일보했다는 것.

 

 일단은 좀 더 공부가 필요한 것 같고, 다음 하드포크로 EIP-3074의 합류가 확정되었는데 이와 관련해서도 차근차근 살펴봐야 할 것 같다.


참고 자료

 

GitHub - privy-io/biconomy-example

Contribute to privy-io/biconomy-example development by creating an account on GitHub.

github.com

 

GitHub - bcnmy/biconomy_web3auth_example: An example repo for integration of Biconomy with Web3Auth social login provider.

An example repo for integration of Biconomy with Web3Auth social login provider. - bcnmy/biconomy_web3auth_example

github.com

 

How Biconomy Paymaster Works - Sponsor gas or pay in any token | Biconomy

 

www.biconomy.io

 

최근에 올라온 글
최근에 달린 댓글
«   2025/01   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Total
Today
Yesterday
글 보관함