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

기존 코드를 보면
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 )
블록체인의 가장 기본 중에 하나가 분산원장이듯이,
이번에는 매니저의 권한을 여러 주소에서 동의를 받아 실행하는 방식으로 바꾸는 것을 진행하였다.
// 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?
더찾아보기 - 추후링크첨부
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 으로 바꿔줌
...
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 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 편집창 작성 방법 -> 이전 글 참고...