8-2. mint 함수 접근권한 제한하기 (access control)

동동주·2025년 11월 2일


Todo :
0. 기존 문제상황
1. access control (modifier 사용)
2. access control module




0. 기존 문제상황

contracts/MyToken.sol


mint function 이전 7-2. TinyBank) 입금(withdraw) & 보상(reward) 기능에서 다뤘듯이,
Staking Reward를 주기 위해 mint() external 함수를 새로 만들었었다.

위의 함수는 이전의 mint() internal 과는 달리, external이라 누구나 접근해서 코인을 만들어낼 수 있다. 따라서 접근 제한기능이 꼭 필요했지만, 당시에는 일단 넘어갔었다.

test/MyToken.ts

describe("Mint", () => {
    .
    .
    .

    it("should return or revert when minting infinitly", async () => {
      const hacker = signers[2];
      const mintingAgainAmount = hre.ethers.parseUnits("100", DECIMALS);
      await myTokenC.connect(hacker).mint(mintingAgainAmount, hacker.address);
      console.log(
        hre.ethers
          .formatUnits(await myTokenC.balanceOf(signer2.address), DECIMALS)
          .toString() + "MT",
      );
    });
  });

테스트 코드에서 보이듯이 기존 mint 함수는 발행자(signer0)가 아니어도
누구나(signer2와 같은 hacker도) mint 실행이 가능하다.

실제로 저 코드를 테스트하면, 입력한 mintingAgainAmount 값만큼 토큰이 문제없이 발행되어서 100MT가 터미널에 출력된다.
( mintingAgainAmount = 100 일 때 )




1. access control (modifier 사용)

목표 테스트 코드

+ TDD : Test Driven Development ...

   it("should return or revert when minting infinitly", async () => {
      const hacker = signers[2];
      const mintingAgainAmount = hre.ethers.parseUnits("10000", DECIMALS);
      await expect(
        myTokenC.connect(hacker).mint(mintingAgainAmount, hacker.address),
      ).to.be.revertedWith("You are not authorized");
    });
  });

최초 발행자가 아닌 외부인이 mint 를 실행하려고 하면 권한이 없다는 오류가 정상적으로 뜨도록 하는 것이 목표이다.

현재 터미널 출력

should return or revert when minting infinitly:
AssertionError: Expected transaction to be reverted with reason 'You are not authorized', but it didn't revert

아직 아무것도 안했으니 당연히 예상한 오류와 다르다는 결과를 볼 수 있다.


contract/MyToken.sol

// 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);
    
    address public owner; //추가
    address public manager; //추가
    string public name;
    string public symbol;
    uint8 public decimals;
.
.
.
    constructor(string memory _name, string memory _symbol, uint8 _decimals, uint256 _amount) {
        owner = msg.sender; //추가
        manager = msg.sender; //추가 : 일단 owner로 초기화
        name = _name;
        symbol = _symbol;
      ...
    }
.
.
.
.
    //modifier 배우고 수정하려던 부분 : modifier 추가
	// 기존에 owner 였던 변수명 to로 변경
    function mint(uint256 amount, address to) external onlyManaer { 
        _mint(amount, to);
    } 

	// 추가
    function setManager(address _manager) external onlyOwner {
        manager = _manager;
    }

.
.
.
.
    modifier onlyOwner {
        require(msg.sender == owner, "You are not authorized");
        _;
    }

    modifier onlyManager {
        require(msg.sender == manager, 
        "You are not authorized to manage this token"
        );
        _;
    }
      

코드 흐름

  • onlyOwner (modifier)
    최초 배포자를 저장하기 위한 owner라는 주소를 생성하고, 생성자에서 주소를 할당한다. modifier 내부에서는 owner가 실행했는지 주소를 확인 하고 함수본문을 실행하도록 한다.
    *이 때 기존의 owner라는 변수 명과 겹치므로, 기존 ownerto로 변경.

  • onlyManager (modifier)
    다만, onlyOwner만 존재하는 경우 TinyBank가 stake같은 기능을 못하게 된다. 따라서, manager라는 주소로 approve를 받아 거래를 진행하는 TinyBank를 지정할 것이다.
    다만, 생성자에서는 일단 최초 배포자를 저장한다. (이후 TinyBank.ts에서 TinyBank로 다시 저장함)
    modifier 내부에서는 manager가 실행했는지 확인 후 함수본문을 실행하도록 설정한다.


test/TinyBank.ts

import hre from "hardhat";
.
.
.

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

  beforeEach(async () => {
    signers = await hre.ethers.getSigners();
    myTokenC = await hre.ethers.deployContract("MyToken", [
      "MyToken",
      "MT",
      DECIMALS,
      MINTING_AMOUNT,
    ]);
    tinyBankC = await hre.ethers.deployContract("TinyBank", [
      await myTokenC.getAddress(),
    ]);
    //아래 추가
    await myTokenC.setManager(tinyBankC.getAddress()); 
  });
  
  
  describe("Initialized state check", () => { ....
  ....
  ....

그리고 테스트 시 manager를 TinyBank로 지정하는 내용을 beforEach에 적어서 다른 모든 테스트에 적용되도록 한다.




2. access control module

유지,관리가 용이하도록 위에서 만든 modifier들을 따로 파일을 만들어서 모아준다.

contracts/ManagedAccess.sol

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

abstract contract ManagedAccess {
    address public owner;
    address public manager;
    constructor(address _owner, address _manager) {
        owner = _owner;
        manager = _manager;
    }

    modifier onlyOwner {
        require(msg.sender == owner, "You are not authorized");
        _;
    }

    modifier onlyManager {
        require(
            msg.sender == manager, 
        "You are not authorized to manage this token"
        );
        _;
    }
    
}

코드 흐름

  • 이 컨트랙트는 직접 실행되는 것이 아니므로 abstract로 선언한다.

  • 각각 ownermanager 주소 선언, modifier 코드를 MyToken.sol에서 잘라서 옮겨온다. (MyToken에서 필요x)


contracts/MyToken.sol

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

import "./ManagedAccess.sol";

contract MyToken is ManagedAccess {
    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)) public allowance;


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

코드 흐름

  • 위에서 말했듯 ownermanager 주소 선언, modifier 코드 부분 제거

  • ManagedAccess.sol 파일 import

  • MyToken이 ManagedAccess 를 상속받는다 (Solidity에서는 is로 상속)
    그러고 보니 자바 배울 때 상속은 is-a관계라고 배운적이 있다(Car is a Vehicle)
    자바에서는 extend로 상속을 받지만, Solidiy에서 is로 상속을 받는 이유도 이런 상속의 개념과 연관이 있는 게 아닐까....?

  • 생성자 옆에서 상속받은 생성자 호출
    constructor(~~) ManagedAccess(msg.sender, msg.sender) { ~~~ }




git commit

❯ git add contracts/MyToken.sol contracts/ManagedAccess.sol
❯ git commit -m "feat: add access manager"

❯ git add test/MyToken.ts                                     
❯ git commit -m "feat: add malicious minting try test"

❯ git add test/MyToken.ts                                     
❯ git commit -m "test: add malicious minting try test" 

❯ git add test/TinyBank.ts                                     
❯ git commit -m "test(TinyBank): grant manager permission to TinyBank"

마지막에 쓰다보니 git log로 볼 때 어느 파일에 추가한 건지 모르니까 스코프가 필요하구나... 싶어서 test(TinyBank)로 스코프 추가했어요
이전꺼는 굳이 수정하진 않음...

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

0개의 댓글