무지개곰
article thumbnail

approve와 transferFrom이 익숙하지만 비슷한 기능을 해주는 permit이란 것에 대하여 알아보도록 하겠습니다.

목차

ERC20 Permit은 무엇인가?

Permit이 가능한 Token 생성

Permit 사용 예시

Permit을 위한 서명


ERC20 Permit은 무엇인가?

ERC20 토큰에는 approve라는 Token에 대한 권한을 특정 address에게 승인하는 기능이 있습니다. approve이후에 상대방이 transferFrom을 통하여 나의 토큰을 가져갈 수 있습니다. 이러한 경우 토큰 사용자가 contract에 approve를 해주는 transaction과 승인한 token을 사용하기 위하여 contract의 method를 실행하는 transaction 두 가지의 transaction을 사용하게 되는 번거로움이 있습니다.

 

ERC20 Permit은 ERC20 표준 토큰 contract에 추가된 기능으로 사용자가 approve를 위한 트랜잭션을 발생시키지 않고 토큰을 전송할 수 있도록 구현되어 있습니다. 전자 서명 정보인 v, r, s를 이용하여 approve transaction을 생략할 수 있습니다.

 

ERC20 Permit을 구현하는 인터페이스 예시는 아래와 같습니다.

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

interface IERC20Permit {
    
    function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external;

    function nonces(address owner) external view returns (uint256);
    
    function DOMAIN_SEPARATOR() external view returns (bytes32);
}

permit

 owner : 토큰의 소유자 address

 spender : 권한을 부여받을 address

 value : 권한을 부여할 token의 양

 deadline : permit의 유효기간 

 v, r, s : owner의 전자 서명

각 값을 통하여 토큰의 권한을 부여합니다.

nonces

owner의 현재 nonce값을 반환하여 승인 트랜잭션의 고유성을 보장하기 위해 사용합니다.

DOMAIN_SEPARATOR

EIP-712 서명 도메인 분리자를 반환합니다. 승인 트랜잭션의 유효성을 검사하는 데 사용됩니다.


Permit이 가능한 Token 생성

openzeppelin을 이용하여 permit가능한 Token을 만들면 아래와 같습니다.

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

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";

contract SampleToken is ERC20, ERC20Permit {
    constructor(string memory name, string memory symbol) ERC20(name, symbol) ERC20Permit(name) {
        
    }
    // 추가로 원하는 code 작성
}

permit은 토큰의 권한을 부여하는 방법이므로 contract에 ERC20을 상속합니다.

permit이 정의되어 있는 ERC20 Permit을 openzeppelin을 이용하여 상속해 줍니다.

constructor를 이용하여 ERC20 토큰의 이름과 symbol을 전달해 주고 ERC20 Permit도 token의 이름을 전달해 줍니다.


Permit 사용 예시

Contract 예시

permit을 이용하여 토큰을 전송하는 contract 예시 아래와 같습니다.

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

contract MyContract {
    IERC20Permit public token;
    mapping(address => amount) public deposit;
    
    constructor(address tokenAddress) {
        token = IERC20Permit(tokenAddress);
    }
    
    function depositWithPermit(uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external {
        token.permit(msg.sender, address(this), amount, deadline, v, r, s);
        token.transferFrom(msg.sender, address(this), amount);
        deposit[msg.sender] += amount;
    }
}

constructor의 parameter로 token 생성 예시를 통하여 생성한 token adress를 입력합니다.

transferWithPermit을 실행시키게 되면 token.permit이 실행되어 msg.sender와 v, r, s 서명자가 동일한지 확인하고 address(this)에게 amount만큼 approve를 해줍니다.

adress(this) 즉 위의 예시인 MyContract가 권한을 부여받았기 때문에 transferFrom을 통하여 recipient에게 amount를 전달합니다.

 

위의 예시의 경우 token을 contract에 전달을 하면 deposit에 해당 address가 입금한 token의 수량을 기록합니다. 기존의 approve를 이용하여 transferFrom을 하였다면 사용자는 approve를 위한 transaction과 deposit을 하기 위한 transaction 총 두 번의 transaction을 발생시켜야 하지만 permit을 이용한다면 한 번의 transaction으로 deposit이 가능합니다.


Permit을 위한 서명

permit을 실행시키려면 v, r, s를 구해 parameter로 전달하여야 합니다. 서명을 구하여 v, r, s를 얻는 예시를 설명하겠습니다.

ethers.js를 받아 import 해주셔야 합니다. 필요한 것은 2가지로 BrowserProvider와 ethers입니다.

import { BrowserProvider, ethers } from "ethers";

 아래는 v, r, s를 얻는 과정입니다.

const getV_R_S = async()=>{
  const metamaskProvider = new BrowserProvider(web3Provider);
  const permitSigner = await metamaskProvider.getSigner();

  await window.ethereum.request({ method: "eth_requestAccounts" });

  const signature = await permitSigner.signTypedData(
    {
      name: "Contract Name",
      version: "1",
      chainId: Chain ID,
      verifyingContract: "Contract Address",
    },
    {
      title: [
        {
          name: "owner",
          type: "address",
        },
        {
          name: "spender",
          type: "address",
        },
        {
          name: "value",
          type: "uint256",
        },
        {
          name: "nonce",
          type: "uint256",
        },
        {
          name: "deadline",
          type: "uint256",
        },
      ],
    },
    {
      owner: permitSigner.address,
      spender: "권한을 줄 address",
      value: 권한을 줄 토큰 수량,
      nonce: 0,
      deadline: Date.now() + 1000*60*60, //1시간 예시
    }
  );

  console.log("Signature:", signature);

  const sig = ethers.Signature.from(signature);
  console.log("v:", sig.v);
  console.log("r:", sig.r);
  console.log("s:", sig.s);
}

getV_R_S라는 함수를 생성하였고 그 안에서 v, r, s를 구하도록 만들었습니다.

signTypedData의 parameter는 3개이며 순서대로 domain 정보, 서명 메시지 이름과 그에 해당하는 필드 정보, 서명에 들어갈 값입니다.

첫 번째 parameter인 domain정보의 경우 해당되는 chainID와 contract address를 입력합니다.

두 번째 parameter인 서명 메시지 이름과 필드 정보는 객체의 key가 메시지 이름이 되고 객체의 value는 배열로 작성합니다.

세 번째 parameter인 서명에 들어갈 값은 해당되는 값을 입력합니다.

 

v, r, s를 얻는 방법은 signTypedData를 이용하는 방법과 ethers.solidityPacked를 이용한 방법이 있습니다.

solidityPacked의 경우 서명할 메시지가 16진수로 보이는 반면 signTypedData의 경우 서명할 메시지가 어떠한 정보를 담고 있는지 확인할 수 있어서 signTypedData가 직관적이라 추천드립니다.

 

'BlockChain > solidity' 카테고리의 다른 글

[Solidity] ERC-2771이란? (ERC-2771Context란?)  (0) 2023.07.02
[Solidity] EIP-712란?  (0) 2023.07.02
[Solidity] ERC-1155란?  (0) 2023.05.18
[Solidity] ERC-721이란?  (0) 2023.05.18
[Solidity] ERC20이란? (ERC20 토큰 발행)  (0) 2023.05.14
profile

무지개곰

@무지개곰

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!