[Solidity] ERC20 Offchain Approve

임형석·2023년 11월 21일
0

Solidity


ERC20Permit

ERC20Permit 은 오픈제플린의 ERC20 Extension 라이브러리 중 하나이다.

https://eips.ethereum.org/EIPS/eip-2612

기존에는 사용자가 ERC20 토큰을 전송하기 위해서는 dApp 에 토큰의 제어 권한 상태를 업데이트하는 Approve 트랜잭션을 보내고 컨펌이 된 후에서야 dApp 을 통해 토큰의 전송이 가능했다.

EIP-2612 로 제안되었으며, 오프체인에서 전자서명을 생성하는 방식으로 Approve 하여 한번의 트랜잭션으로 ERC20 토큰의 전송이 가능하도록 만든 것이다.

트랜잭션을 최소화하여 가스비를 절약할 수 있으며, 토큰 전송에 걸리는 시간도 단축할 수 있으므로 유저가 dApp 을 이용하기 위해 오래 기다릴 필요가 없다.

ERC20Permit 에 추가된 것은 세개의 함수이다.

function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external
function nonces(address owner) external view returns (uint)
function DOMAIN_SEPARATOR() external view returns (bytes32)

permit 사용자가 오프체인에서 생성한 서명이 유효한지 확인하는 함수이다.
nonces Signature replay 공격을 막기 위한 방법으로, 사용자가 생성한 서명이 악용되어 여러번 쓰이는 것을 막는다.
DOMAIN_SEPARATOR() EIP-712 로 제안된 것이며, nonces 와 같이 Signature replay 공격을 막기 위한 방법 중 하나로 사용자가 생성한 서명이 악용되어 다른 컨트랙트에서 사용되는 것을 막는다.


Contract

직접 배포하고 테스트하기 위해서 아래와 같이 컨트랙트를 작성한다.

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

contract TESTUSDT is ERC20, ERC20Permit {

    constructor(uint amount) ERC20("TEST USDT", "USDT") ERC20Permit("TEST USDT") {
        _mint(msg.sender, amount);
    }

    function decimals() public override pure returns(uint8){
        return 0;
    }
}

dapp 컨트랙트도 작성한다. 이 컨트랙트의 transferWithPermit 를 통해 서명의 유효성을 확인하고 토큰을 전송한다.

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

contract dapp {
    ERC20Permit public immutable token;

    constructor(address _token){
        token = ERC20Permit(_token);
    }

    function transferWithPermit(address to, uint amount, uint deadline, uint8 v, bytes32 r, bytes32 s) external {
        token.permit(msg.sender, address(this), amount, deadline, v, r, s);
        token.transferFrom(msg.sender, to, amount);
    }
}

permit 함수가 서명의 유효성을 확인할 때 7개의 파라미터가 필요하다.

서명을 생성한 주소, approve 할 주소, 전송할 토큰의 갯수, 서명의 유효기간, v, r, s 이다.

v, r, s 값은 EIP-712 에서 나온 eth_signTypedData_v4 에 몇가지 파라미터만 넣으면 구할 수 있다.

파라미터의 구조는 다음과 같다. 조금 복잡하다.

        const dataToSign = JSON.stringify({
          types: {
            EIP712Domain: [
        { name: "name", type: "string" },
        { name: "version", type: "string" },
        { name: "chainId", type: "uint256" },
        { name: "verifyingContract", type: "address" },
      ],
            Permit: [
          { name: "owner", type: "address" },
          { name: "spender", type: "address" },
          { name: "value", type: "uint256" },
          { name: "nonce", type: "uint256" },
          { name: "deadline", type: "uint256" },
        ],
          },
          domain: {
        name: domainName,
        version: domainVersion,
        verifyingContract: contractAddress,
        chainId,
      },
          primaryType: "Permit",
          message: { owner: account, spender, value, nonce, deadline },
        });

EIP-712 에서 제안된 구조로 아래의 파라미터에 서명을 생성한 주소, approve 할 주소, 전송할 토큰의 갯수, 서명의 유효기간이 들어가고, 이 파라미터를 해싱하는 과정을 통해 v, r, s 값을 생성하게 된다.

v, r, s 값을 연결시키면 하나의 서명이 생성된다.

서명을 생성하는 주소는 컨트랙트가 될 수 없다. EOA 만이 서명을 생성할 수 있다.


서명 생성

https://gist.github.com/shobhitic/c16b647562e7995d788e2e1bd5818267

이 링크를 참조했다. 수정할 코드만 살펴보면,

18~21 번째 줄

      const domainName = "TEST USDT"; // 토큰의 이름을 정확하게 입력
      const domainVersion = "1"; // 도메인 버전은 1
      const chainId = 11155111; // 리믹스 = 1, goerli = 5, sepolia = 11155111, 원하는 네트워크로 배포
      const contractAddress = "0x5D9CA9C406cf820b04E11e845A73f665645e3F6c"; // 토큰 컨트랙트 주소 입력

56 번째 줄 네트워크는 세폴리아로 바꾸어준다.

        network: "sepolia", // optional

133~138 번째 줄은 직접 수정한다.
dapp 컨트랙트 주소, 전송할 토큰 갯수, nonces 값, deadline 값이다.

        const permit = await createPermit(
          "0xD379D7f8A85D46f8EAAE8F98B79c658833929cE5", // dapp 컨트랙트 주소
          100, // 전송할 토큰의 갯수
          0, // nonces. 지갑이 이 컨트랙트를 통해 몇번 서명을 생성하고 트랜잭션을 실행하였는지
          2333122312 // deadline 값. 현재 시간보다 커야함 (unix time)
        );

여기까지 수정을 완료했다면, .html 확장자로 파일을 저장한다.

파일을 저장한 폴더를 기준으로 콘솔창을 열어서 python -m http.server 로 http 서버를 열어준다. 파이썬이 깔려있어야 한다.

http://localhost:8000/ 으로 들어가서 콘솔창에 await main() 을 입력 후, 메타마스크를 선택하면 이렇게 서명 요청 화면이 뜬다.

서명을 클릭하면 콘솔창에 v, r, s 값을 확인할 수 있다. 이 값이 합쳐지면 서명이 된다.


토큰 전송

위의 v, r, s 값을 이용해서 transferWithPermit 함수를 호출한다.

트랜잭션 한번으로 100개의 토큰이 전송된 것을 확인할 수 있다.

생성한 서명으로 트랜잭션이 실행되면 nonces 값이 1 올라간다.


오프체인에서 서명을 생성하고 토큰을 전송해보았다. 생각보다 서명을 생성하는 과정이 복잡하다.
서명을 생성하는 과정에서 ECDSA 라는 라이브러리를 사용하는데, "Elliptic Curve Digital Signature Algorithm" 로, 비트코인의 지갑 생성과정에서도 사용된다. 블록체인에서 많이 사용되는 듯 하니 알아두면 좋을 것 같다..

트랜잭션, 수수료 그리고 유저 경험에서도 기존의 ERC20 과는 비교할 수가 없을 것 같다.
단점을 굳이 꼽자면 서명 생성을 위한 코드를 짜는게 조금 복잡하다. 그래도 자바스크립트로 서명을 생성하는 컴포넌트를 만들어두면 나중에도 언제든 사용할 수 있을 것 같다.


0개의 댓글