6-3. 토큰 이동 권한 승인 (approve) 함수

동동주·2025년 10월 17일
  • approve
    다른 사람이 내 토큰을 보낼 수 있게 허용하는 것
    (allow spender address to send my token)
    잘 사용하면 편리하지만, 신뢰하지 못하는 사람에게 위임할 경우 매우 위험
    [디센트] 지갑 UX 스토리 : “approve” 그 특별한 트랜잭션에 관하여 해당 글에 사용하는 경우가 적혀있어서 참고...,,

  • transferFrom
    위임받은 사람이 나의 토큰을 옮길 수 있다
    spender: owner -> target address

일반적인 거래 (직접 전송)
token owner --> bank contract

token owner --(approve로 권한 승인)--> router contract --> bank contract
토큰을 여러번 보내는 거래일 경우 (multi contract)
권한을 위임하는 방식이 편리하다..
contract는 여러 개의 contract를 호출할 수 있다...?

상품이 변해도 ui같은게 변경되지 않아도 됨
거래가 적게 일어남

contract 코드

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

contract MyToken {
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed spender, uint256 amount);
    
    string public name;
    string public symbol;
    uint8 public decimals;

    //토큰의 총 발행 개수
    uint256 public totalSupply;

    //누가 얼마나 가지고 있는지
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) allowance;


    constructor(string memory _name, string memory _symbol, uint8 _decimals, uint256 _amount) {
        name = _name;
        symbol = _symbol;
        decimals = _decimals;
        _mint(_amount *10**uint256(decimals), msg.sender); // _amount만큼 MT 발행
    }

    function _mint(uint256 amount, address owner) internal {
        totalSupply += amount;
        balanceOf[owner] += amount; 
                
        emit Transfer(address(0), owner, amount);
    }

    function transfer(uint256 amount, address to) external {
        require(balanceOf[msg.sender] >= amount, "insufficient balance");

        balanceOf[msg.sender] -= amount; 
        balanceOf[to] += amount; 

        emit Transfer(msg.sender, to, amount);
    }



    function approve(address spender, uint256 amount) external {
        allowance[msg.sender][spender] = amount;

        emit Approval(spender, amount);
    }

    function TransferFrom(address from, address to, uint256 amount) external {
        address spender = msg.sender;
        require(allowance[from][spender] >= amount, "insufficient allowance");
      	require(balanceOf[from] >= _amount, "insufficient balance"); //코드추가 (25/11/13 빠진부분 추가함)
        allowance[from][spender] -= amount;
        balanceOf[from] -= amount;
        balanceOf[to] += amount;
        
        emit Transfer(from, to, amount);
    }
}

owner --> spender(amount)


git commit

❯ git add test/MyToken.ts                                     
❯ git commit -m "transfer emit test"                           

❯ git add .                                                   
❯ git commit -m "added approve and transferFrom" 


test 코드

import hre from "hardhat";
import { expect } from "chai";
import { MyToken } from "../typechain-types";
import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers";
import { parseUnits } from "ethers";
// hardhat에서 컴파일 할 때 필요한 타입 정의 파일을 여기에 보관함

const mintingAmount = 100n;
const decimals = 18n;

describe("My Token", () => {
  let myTokenC: MyToken;
  let signers: HardhatEthersSigner[];

  // before > describe, let myTokenC > myTokenC 사용ok
  beforeEach("should deploy", async () => {
    signers = await hre.ethers.getSigners();
    myTokenC = await hre.ethers.deployContract("MyToken", [
      "MyToken",
      "MT",
      decimals,
      mintingAmount, //100MT 발행
    ]);
  });

  describe("Basic state value check", () => {
    it("should return name", async () => {
      expect(await myTokenC.name()).equal("MyToken");
    });
    it("should return symbol", async () => {
      expect(await myTokenC.symbol()).equal("MT");
    });
    it("should return decimals", async () => {
      expect(await myTokenC.decimals()).equal(decimals);
    });

    it("should retrun 100 totalSupply", async () => {
      expect(await myTokenC.totalSupply()).equal(
        mintingAmount * 10n ** decimals,
      );
    });
  });

  describe("Mint", () => {
    // 1MT = 1*(10^18) = 1n*10n**18n = BigInt(1*10**18)
    it("should retrun 1MT balance for signer 0", async () => {
      expect(await myTokenC.balanceOf(signers[0].address)).equal(
        mintingAmount * 10n ** decimals,
      );
    });
  });

  describe("Transfer", () => {
    it("should have 0.5MT", async () => {
      const signer0 = signers[0];
      const signer1 = signers[1];
      await expect(
        myTokenC.transfer(
          hre.ethers.parseUnits("0.5", decimals),
          signer1.address,
        ),
      )
        .to.emit(myTokenC, "Transfer")
        .withArgs(
          signer0.address,
          signer1.address,
          hre.ethers.parseUnits("0.5", decimals),
        );
      expect(await myTokenC.balanceOf(signer1.address)).equal(
        hre.ethers.parseUnits("0.5", decimals),
      );
    });
    it("should be reverted with insufficient balance error", async () => {
      const signer1 = signers[1];
      await expect(
        myTokenC.transfer(
          hre.ethers.parseUnits((mintingAmount + 1n).toString(), decimals),
          signer1.address,
        ),
      ).to.be.revertedWith("insufficient balance");
    });
  });

  describe("TransferForm", () => {
    it("should emit Approval event", async () => {
      const signer1 = signers[1];
      await expect(
        myTokenC.approve(signer1, hre.ethers.parseUnits("10", decimals)),
      )
        .to.emit(myTokenC, "Approval")
        .withArgs(signer1.address, hre.ethers.parseUnits("10", decimals));
    });
    it("should be reverted with insufficient allowance error", async () => {
      const signer0 = signers[0];
      const signer1 = signers[1];
      await expect(
        myTokenC
          .connect(signer1)
          .transferFrom(
            signer0.address,
            signer1.address,
            hre.ethers.parseUnits("1", decimals),
          ),
      ).to.be.revertedWith("insufficient allowance");
    });

    it("should emit Approval event and transfer from signer0 to signer1", async () => {
      const signer0 = signers[0];
      const signer1 = signers[1];

      //approve
      await expect(
        myTokenC.approve(signer1, hre.ethers.parseUnits("10", decimals)),
      )
        .to.emit(myTokenC, "Approval")
        .withArgs(signer1.address, hre.ethers.parseUnits("10", decimals));

      //transferFrom
      await expect(
        myTokenC
          .connect(signer1)
          .transferFrom(
            signer0.address,
            signer1.address,
            hre.ethers.parseUnits("5", decimals),
          ),
      )
        .to.emit(myTokenC, "Transfer")
        .withArgs(
          signer0.address,
          signer1.address,
          hre.ethers.parseUnits("5", decimals),
        );

      expect(await myTokenC.balanceOf(signer1.address)).equal(
        hre.ethers.parseUnits("5", decimals),
      );
    });
  });
});

터미널

❯ npx hardhat test                                             

  My Token
    Basic state value check
      ✔ should return name (57ms)
      ✔ should return symbol (45ms)
      ✔ should return decimals
      ✔ should retrun 100 totalSupply
    Mint
      ✔ should retrun 1MT balance for signer 0
    Transfer
      ✔ should have 0.5MT (78ms)
      ✔ should be reverted with insufficient balance error (43ms)
    TransferForm
      ✔ should emit Approval event (43ms)
      ✔ should be reverted with insufficient allowance error
      ✔ should emit Approval event and transfer from signer0 to signer1 (123ms)


  10 passing (3s)

git commit

git commit -m "approve and transferFrom test"

(아마 이 때 한 것 같은데..)

+ 코드 수정 & git commit

A. fix allowance scope to public

  • contracts/MyToken.sol
    allowance 부분 --> public 추가
//누가 얼마나 가지고 있는지
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

❯ git commit -m "fix allowance scope to public"


B-1. constant renaming

  • test/MyToken.ts
    상수 변수명 변경 (대문자 표기)
const MINTING_AMOUNT = 100n;
const DECIMALS = 18n;

❯ git commit -m "constant renaming"


B-2. refactor constant

  • test/constant.ts 만들고 아래 작성
export const MINTING_AMOUNT = 100n;
export const DECIMALS = 18n;
  • test/MyToken.ts
    MyToken 파일에서 중복되는 아래 코드 삭제 후
const MINTING_AMOUNT = 100n;
const DECIMALS = 18n;

import자리에 아래 코드 작성.

import { DECIMALS, MINTING_AMOUNT } from "./constant";

❯ git add test/constant.ts
❯ git add test/MyToken.ts
❯ git commit -m "refactor constant"

profile
배운 내용 정리&기록, 스크랩

0개의 댓글