9-1. Decentralized access control (분산형 접근 제어)

동동주·2025년 11월 6일


Todo :
0. 기존의 access control 방식
1. access control module 추가
2. test코드 작성




0. 기존의 access control 방식

기존코드

기존 코드를 보면
owner, manager는 단 하나의 주소이고, 이 주소에게 토큰 발행부터 리워드 변경까지 모든 권한이 집중되는 것을 볼 수 있다.

Centralization vs Decentralization
a single DB vs Distributed Ledger
a single BlockChain Node vs BlockChain Node network

블록체인에서의 Governance --> voting
( abc agenda --> by vote --> decision )

블록체인의 가장 기본 중에 하나가 분산원장이듯이,
이번에는 매니저의 권한을 여러 주소에서 동의를 받아 실행하는 방식으로 바꾸는 것을 진행하였다.




1. access control module 추가

contracts/MultiManagedAccess.sol

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

abstract contract MultiManagedAccess {
    uint constant MANAGER_NUMBERS = 5;

    address public owner; //owner는 그대로
    address[MANAGER_NUMBERS] public managers;
    bool[MANAGER_NUMBERS] public confirmed; //승인 여부 저장

  
    constructor(address _owner, address[MANAGER_NUMBERS] memory _managers) {
        owner = _owner;
        for(uint i=0; i<MANAGER_NUMBERS; i++) {
            managers[i] = _managers[i];
        }
    } //deep copy

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

    function allConfirmed() internal view returns(bool) {
        for(uint i=0; i<MANAGER_NUMBERS; i++) {
            if(!confirmed[i]){ 
                return false; //한 명이라도 confirm X
            }
        }
        return true; //전부 confirm OK
    }

    function reset() internal { //기록 초기화
        for(uint i=0; i<MANAGER_NUMBERS; i++) {
            confirmed[i] = false;
        }
    }

    modifier onlyAllConfirmed { 
      //매니저가 호출했는지 확인
        bool isManager = false;
        for(uint i=0; i<MANAGER_NUMBERS; i++) {
            if(managers[i] == msg.sender) {
                isManager = true;
                break;
            }
            require(isManager, "You are not a managers");
        }
        // 모든 매니저가 동의했는지 확인
        require(allConfirmed(),"Not all confirmed yet");
        // confirm 기록 초기화
        reset();
        _;
    }

    function confirm() external {
        bool found = false;
        for(uint i=0; i<MANAGER_NUMBERS; i++) {
            if(managers[i] == msg.sender) { //관리자라면
                found = true;
                confirmed[i] = true; //confirm 상태 변경
                break;
            }
        }
        require(found, "You are not a managers");
    }
}

코드 흐름

  • owner & onlyOwner 기능은 동일

  • MANAGER_NUMBERS의 수만큼 managers 배열에 관리자 등록

  • confirm()에서 매니저인지 확인 → bool[] confirmed에 저장

  • onlyAllConfirmed()
    매니저인지 확인 후, allConfirmed()로 확인하고 reset()으로 배열 기록 초기화

  • deepcopy?
    더찾아보기 - 추후링크첨부




2. test코드 작성

contracts/TinyBank.sol 변경

import "./MultiManagedAccess.sol"; //변경

...

contract TinyBank is MultiManagedAccess {
  ...
  
    constructor(IMyToken _stakingToken, address[MANAGER_NUMBERS] memory _managers) MultiManagedAccess(msg.sender, _managers) { 
        stakingToken = _stakingToken;
        rewardPerBlock = defaultRewardPerBlock;
    }

    function setRewardPerBlock(uint256 _amount) external onlyAllConfirmed {
        rewardPerBlock = _amount;
    }

코드 흐름

  • 만든 MultiManagedAccess (access control module) 파일 import

  • MultiManagedAccess 상속 (is)

  • modifier를 onlyManager 에서 onlyAllConfirmed 으로 바꿔줌


test/TinyBank.ts

...

 beforeEach(async () => {
    signers = await hre.ethers.getSigners();
    myTokenC = await hre.ethers.deployContract("MyToken", [
      "MyToken",
      "MT",
      DECIMALS,
      MINTING_AMOUNT, //100MT 발행했었다
    ]);
    tinyBankC = await hre.ethers.deployContract("TinyBank", [
      await myTokenC.getAddress(),
      [
        signers[0].address,
        signers[1].address,
        signers[2].address,
        signers[3].address,
        signers[4].address,
      ],
    ]);
    await myTokenC.setManager(tinyBankC.getAddress());
  });
  
  ...
  
  describe("reward", async () => {
    it("should reward 1MT every blocks", async () => {
      const signer0 = signers[0];
      const stakingAmount = hre.ethers.parseUnits("50", DECIMALS);
      await myTokenC.approve(await tinyBankC.getAddress(), stakingAmount);
      await tinyBankC.stake(stakingAmount);
      const BLOCKS = 5n;
      const transferAmount = hre.ethers.parseUnits("1", DECIMALS);
      for (var i = 0; i < BLOCKS; i++) {
        await myTokenC.transfer(transferAmount, signer0.address);
      }
      await tinyBankC.withdraw(stakingAmount);
      expect(await myTokenC.balanceOf(signer0.address)).equal(
        hre.ethers.parseUnits((BLOCKS + MINTING_AMOUNT + 1n).toString()),
      );
    });

    it("should update rewardPerBlock and reward 10MT every blocks when all managers confirm", async () => {
      const rewardToChange = hre.ethers.parseUnits("10", DECIMALS);
      const MANAGER_NUMBERS = 5;
      // 매니저 전부 동의
      for (var i = 0; i < MANAGER_NUMBERS; i++) {
        await tinyBankC.connect(signers[i]).confirm();
      }
      // reward 변경
      await tinyBankC.connect(signers[0]).setRewardPerBlock(rewardToChange);

      // reward 적용 확인
      const signer0 = signers[0];
      const stakingAmount = hre.ethers.parseUnits("50", DECIMALS);
      await myTokenC.approve(await tinyBankC.getAddress(), stakingAmount);
      await tinyBankC.stake(stakingAmount);
      const BLOCKS = 5n;
      const transferAmount = hre.ethers.parseUnits("1", DECIMALS);
      for (var i = 0; i < BLOCKS; i++) {
        await myTokenC.transfer(transferAmount, signer0.address);
      }
      await tinyBankC.withdraw(stakingAmount);
      const expectedBalance = 10n * (BLOCKS + 1n) + MINTING_AMOUNT;
      expect(await myTokenC.balanceOf(signer0.address)).equal(
        hre.ethers.parseUnits(expectedBalance.toString()),
      );
    });

    it("should revert if not all managers confirm", async () => {
      const rewardToChange = hre.ethers.parseUnits("1000", DECIMALS);
      const MANAGER_NUMBERS = 5;
      // 매니저 중 일부만 confirm
      for (var i = 0; i < MANAGER_NUMBERS - 3; i++) {
        await tinyBankC.connect(signers[i]).confirm();
      }
      await expect(
        tinyBankC.setRewardPerBlock(rewardToChange),
      ).to.be.revertedWith("Not all confirmed yet");
    });

    it("Should revert when access confirm by hacker", async () => {
      const hacker = signers[9];
      await expect(tinyBankC.connect(hacker).confirm()).to.be.revertedWith(
        "You are not a managers",
      );
    });

    it("Should revert when changing rewardPerBlock by hacker", async () => {
      const hacker = signers[9];
      const rewardToChange = hre.ethers.parseUnits("10000", DECIMALS);
      await expect(
        tinyBankC.connect(hacker).setRewardPerBlock(rewardToChange),
      ).to.be.revertedWith("You are not a managers");
    });
  });

코드흐름

  • deployContract("TinyBank", ~ ) 에서 정의해뒀던 address[MANAGER_NUMBERS] memory _managers 로 사용할 값 넣어주기
    (매니저 주소)

  • 정상적으로 변경 성공해서 리워드 10MT로 받는 거 테스트

  • 일부 매니저만 confirm 한 경우 테스트

  • 각각 해커(매니저가 아닌 사람)가 confirm / setRewardPerBlock 함수를 실행하려고 한 경우 테스트




git commit

❯ git add .                                                   

❯ git commit

    feat(TinyBank): add multi-manager access for setRewardPerBlock
    
    - Implement MultiManagedAccess modifier
    - Replace onlyOwner with onlyAllConfirmed for setRewardPerBlock
    - Add test for authorized rewardPerBlock changes
    - Add tests for unauthorized or incomplete rewardPerBlock changes

git commit 후에 뜨는 nano 편집창 작성 방법 -> 이전 글 참고...

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

0개의 댓글