본문 바로가기
Block Chain/Solidity

[Solidity] Access Control 구현 || Grant & Revoke Role | Solidity 0.8 || KR

by 개발이 체질인 나그네 2022. 10. 1.
반응형

Access Control 이란?

Solidity에서 Access Control이란

권한을 부여받은 account만 function을 실행시킬 수 있도록 Contract에 제어 및 관리 기능을 적용하는 방법론입니다.

 

ERC20 Mint 기능은 다른 사람들이 함부로 실행시켜선 안 되는 함수임으로, 주로 Owner만 실행시킬 수 있습니다.

하지만 여기에 Access Control를 적용하여, Owenr 외 지정된 account도 실행 시킬 수 있도록 권한을 부여할 수 있습니다.

 

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

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";

contract BornToDev is ERC20, AccessControl {
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");

    constructor() ERC20("BornToDev", "BTD") {
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _grantRole(MINTER_ROLE, msg.sender);
    }

    function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) {
        _mint(to, amount);
    }
}

 

- @openzeppelin/contracts/access/AccessControl.sol 

더보기

> openzepplin Access Control

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (access/AccessControl.sol)

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/access/IAccessControl.sol";
import "@openzeppelin/contracts/utils/Context.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
import "@openzeppelin/contracts/utils/introspection/ERC165.sol";

abstract contract AccessControl is Context, IAccessControl, ERC165 {
    struct RoleData {
        mapping(address => bool) members;
        bytes32 adminRole;
    }

    mapping(bytes32 => RoleData) private _roles;

    bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00;

    modifier onlyRole(bytes32 role) {
        _checkRole(role);
        _;
    }

    function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
        return interfaceId == type(IAccessControl).interfaceId || super.supportsInterface(interfaceId);
    }

    function hasRole(bytes32 role, address account) public view virtual override returns (bool) {
        return _roles[role].members[account];
    }

    function _checkRole(bytes32 role) internal view virtual {
        _checkRole(role, _msgSender());
    }

    function _checkRole(bytes32 role, address account) internal view virtual {
        if (!hasRole(role, account)) {
            revert(
                string(
                    abi.encodePacked(
                        "AccessControl: account ",
                        Strings.toHexString(uint160(account), 20),
                        " is missing role ",
                        Strings.toHexString(uint256(role), 32)
                    )
                )
            );
        }
    }

    function getRoleAdmin(bytes32 role) public view virtual override returns (bytes32) {
        return _roles[role].adminRole;
    }

    function grantRole(bytes32 role, address account) public virtual override onlyRole(getRoleAdmin(role)) {
        _grantRole(role, account);
    }

    function revokeRole(bytes32 role, address account) public virtual override onlyRole(getRoleAdmin(role)) {
        _revokeRole(role, account);
    }


    function renounceRole(bytes32 role, address account) public virtual override {
        require(account == _msgSender(), "AccessControl: can only renounce roles for self");

        _revokeRole(role, account);
    }

    function _setupRole(bytes32 role, address account) internal virtual {
        _grantRole(role, account);
    }

    function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual {
        bytes32 previousAdminRole = getRoleAdmin(role);
        _roles[role].adminRole = adminRole;
        emit RoleAdminChanged(role, previousAdminRole, adminRole);
    }

    function _grantRole(bytes32 role, address account) internal virtual {
        if (!hasRole(role, account)) {
            _roles[role].members[account] = true;
            emit RoleGranted(role, account, _msgSender());
        }
    }

    function _revokeRole(bytes32 role, address account) internal virtual {
        if (hasRole(role, account)) {
            _roles[role].members[account] = false;
            emit RoleRevoked(role, account, _msgSender());
        }
    }
}

 

 

※ 지금은 Openzeppelin의 Access Control를 이해하실 필요는 없습니다. 밑에 Clone Coding 실습을 통해 Access Control 방법론에 대해 이해하신 후에 다시 보면 이해가 훨씬 빠를 겁니다.


 

// ERC20 Minting 권한 명
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");


// 권한 부여 기능
function _grantRole(bytes32 role, address account) internal virtual {
        if (!hasRole(role, account)) {
            _roles[role].members[account] = true;
            emit RoleGranted(role, account, _msgSender());
        }
    }

Openzeppelin에서 제공하는 Access Control을 적용한 ERC20 Solidity 코드입니다.

 

contract BornToDev에 MINTER_ROLE을 _grantRole 파라미터에 넣어 실행시키면, 해당 account도 minting을 할 수 있는 권한을 얻습니다. 

이처럼 블록체인 생태계에선 다양한 Smart Contract에 Access Control 방법론을 적용합니다.

 

 

Clone Coding 📝

지금부터 Access Control을 구현하는 방법과 적용하는 방법에 대해 배워보겠습니다.

 

(1) 전체 코드

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

contract AccessControl {

    event GrantRole(bytes32 indexed  role,address indexed account);
    event RevokeRole(bytes32 indexed  role,address indexed account);

    error OnlyRole(bytes32 _role);
	
    // 권한 부여 여부 check
    mapping(bytes32 => mapping(address => bool)) public roles;

    // role
    bytes32 public constant ADMIN = keccak256(abi.encodePacked("ADMIN"));
    bytes32 public constant USER = keccak256(abi.encodePacked("USER"));

	
    modifier onlyRole(bytes32 _role) {
        if(!roles[_role][msg.sender]) {
            revert OnlyRole(_role);
        }
        _;
    }
	
    // contract owner에 ADMIN 권한 부여
    constructor() {
        _grantRole(ADMIN, msg.sender);
    }


    function grantRole(bytes32 _role, address _account) external onlyRole(ADMIN)  {
        _grantRole(_role,_account);
    }

    function _grantRole(bytes32 _role, address _account) internal {
        roles[_role][_account] = true;
        emit GrantRole(_role, _account);
    }

    function revokeRole(bytes32 _role, address _account) external onlyRole(ADMIN)  {
        _revokeRole(_role,_account);
    }
    
    function _revokeRole(bytes32 _role, address _account) internal {
        roles[_role][_account] = false;
        emit RevokeRole(_role, _account);
    }

}

 

 

Access Control을 구현하는 가장 기본 기능입니다.

 

1. 역할 명을 정한다.(ex ADMIN, USER...)

2. account에 정해진 권한이 부여됐는지 확인할 수 있어야 한다.

3. 권한을 부여하는 건 Owner(contract creater)가 최초로 가져야 한다.

4. 권한 부여(grant role) & 권한 취소(revoke role)를 구현한다.

5. 함수에 적용할 modifier 구현한다.

 

1~5는 AccessControl Contract에 대한 기능 설명입니다.

지금부터 하나씩 코드를 설명해보겠습니다.

 

(2) 기능 설명

1. 역할 명을 정한다.(ex ADMIN, USER...)

bytes32 public constant ADMIN = keccak256(abi.encodePacked("ADMIN"));
> 0xdf8b4c520ffe197c5343c6f5aec59570151ef9a492f2c624fd45ddde6135ec42

bytes32 public constant USER = keccak256(abi.encodePacked("USER"));
> 0x2db9fd3d099848027c2383d0a083396f6c41510d7acfd92adc99b6cffcf31e96

 

Contract를 운영할 수 있는 역할은 다양할 수 있습니다. 각 역할을 bytes32 변수로 저장합니다.

String으로 저장하면 데이터가 무겁기에 bytes로 저장합니다.

저장된 역할 변수는 조회만 하고 변경할 일이 없기에, constant로 지정하여 가스비를 절약해줍니다.

 

keccak256(abi.encodePacked("ADMIN"))
keccak256(abi.encodePacked("USER"))

 

Solidity 내에서 ADMIN과 USER는 위 사진처럼 bytes32로 저장됩니다.

 

2. account에 정해진 권한이 부여됐는지 확인할 수 있어야 한다.

// 권한 부여 여부 check
mapping(bytes32 => mapping(address => bool)) public roles;
// roles["account address"]["Role Byte32"]
roles[0x....][ADMIN]
> false or true

 

account에 권한이 부여되어 있다면 true,

부여되지 않았다면 false가 return 됩니다.

 

 

3. 권한을 부여하는 건 Owner(contract creater)가 최초로 가져야 한다.

// contract owner에 ADMIN 권한 부여
constructor() {
    _grantRole(ADMIN, msg.sender);
}

 

constructor를 이용하여, Contract가 최초로 생성될 때, Owner에게 ADMIN 권한을 부여합니다.

 

 

4. 권한 부여(grant role) & 권한 취소(revoke role)를 구현한다.

// 권한 부여
function _grantRole(bytes32 _role, address _account) internal {
   roles[_role][_account] = true;
   emit GrantRole(_role, _account);
}

// 권한 취소
function _revokeRole(bytes32 _role, address _account) internal {
   roles[_role][_account] = false;
   emit RevokeRole(_role, _account);
}

_grantRole와 _revokeRole의 파라미터는 bytes32 _role, address _account 두 가지입니다.

1) _role : 부여할 권한 명(bytes32)

2) _account : 권한을 부여받을 account address

 

ADMIN 권한을 부여 받은 경우 true
ADMIN 권한을 취소 한 경우 false

 

grantRole을 실행시키면 해당 account는 ADMIN 권한에 true가 저장됩니다.

revokeRole를 실행시키면 해당 account는 ADMIN 권한에 false가 저장됩니다.

 

5. 함수에 적용할 modifier 구현한다.

modifier onlyRole(bytes32 _role) {
    if(!roles[_role][msg.sender]) {
       revert OnlyRole(_role);
    }
    _;
}

 

modifier를 만들어서 특정 함수들에 권한 제어를 합니다.

function grantRole(bytes32 _role, address _account) external onlyRole(ADMIN)  {
    _grantRole(_role,_account);
}

function revokeRole(bytes32 _role, address _account) external onlyRole(ADMIN)  {
    _revokeRole(_role,_account);
}

권한을 부여 & 취소하는 기능은 ADMIN만 사용할 수 있어야 합니다.

함수 마지막에 modifier onlyRole(ADMIN)을 넣어 ADMIN 권한을 부여받는 사용자만 함수를 실행할 수 있게 할 수 있습니다.

 

(3) 적용 방법

이제 Access Control를 내가 만드는 Contract에 적용하는 방법을 배워보겠습니다.

 

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

contract myContract is AccessControl {
    
    function onlyAdmin() public view onlyRole(ADMIN) returns(bool) {
        return true;
    }
    function onlyUser() public view onlyRole(USER) returns(bool) {
        return true;
    }
}

onlyAdmin() 함수를 만들었습니다. 이 함수는 msg.sender가 ADMIN 권한을 부여받은 사람만 실행시킬 수 있습니다.

onlyUser() 함수도 마찬가지입니다. msg.sender가 USER 권한을 부여 받은 경우에만 실행 시킬 수 있습니다.

 

이 처럼 AccessControl Contract를 적용하고 싶은 Contract에 상속하여 modify를 통해 적용하면 됩니다.

 

 


 

 

 

반응형

댓글