Crea una cuenta o inicia sesión

¡Continúa aprendiendo sin ningún costo! Únete y comienza a potenciar tu carrera

Curso de Introducción a OpenZeppelin

Curso de Introducción a OpenZeppelin

Sebastián Leonardo Perez

Sebastián Leonardo Perez

Control de acceso basado en roles

3/19
Recursos

En determinados contratos, a veces no es suficiente disponer solo de un owner del mismo. Podemos necesitar tener dos, tres o N cantidad de roles diferentes. Cada uno con sus respectivas responsabilidades.

Contratos con múltiples roles

Podemos definir la cantidad de roles que vamos a necesitar en nuestro contrato. Sin embargo, aún requerimos que exista un único dueño que asigne posteriormente los roles a otras cuentas.

Importa y hereda AccessControl desde OpenZeppelin para comenzar a definir tus propios roles.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

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

contract ContratoConRoles is AccessControl {

    bytes32 public constant ROL_ADMIN = keccak256("ROL_ADMIN");
    bytes32 public constant ROL_USUARIO = keccak256("ROL_USUARIO");

    constructor() {
        _grantRole(ROL_ADMIN, msg.sender);
    }

    function soloAdmin() public  {
        require(hasRole(ROL_ADMIN, msg.sender), "Esta funcion solo puede ser utilizada por el ADMIN");
    }

    function soloUsuario() public  {
        require(hasRole(ROL_USUARIO, msg.sender), "Esta funcion solo puede ser utilizada por un USUARIO");
    }

    function agregarRol(bytes32 role, address account) public {
        require(hasRole(ROL_ADMIN, msg.sender), "Esta funcion solo puede ser utilizada por el ADMIN");

        _grantRole(role, account);
    }

}

La cuenta que despliega el contrato, será la dueña del mismo por defecto, asignando este en el constructor del contrato. Luego de esto, puede utilizar la función agregarRol() para asignar roles a otras cuentas.

La creación de roles se realiza de la forma:

bytes32 public constant ROL_ADMIN = keccak256("ROL_ADMIN");

La función keccak256() encripta y convierte en bytes32 el nombre del rol para poder asignarlo a las cuentas posteriormente de forma segura.

La misma crea una función pública para que puedas obtener el hash del rol y poder utilizarlo.

ROL_ADMIN = 0x7e5b835f8ef15bb117d49cb6d0658113bc08d41d8a6edf12d3d1feb5c5875330
ROL_USUARIO = 0x575582185c1302752a00c2a38ce472577737eb5e4ab29b34daf1ccaa4c9fe08a

La asignación de los roles debe hacerse en bytes32 y no con el String con el que fue definido:

Manipulación de roles

Es importante destacar que, así como el owner del contrato puede asignar roles, también puede quitar roles con la función revokeRole(address).

Las propias cuentas pueden renunciar a un rol si consideran que ya no lo necesita o no lo quieren con la función renounceRole(bytes32,address).

Puede no ser necesario crear un ROL_ADMIN, ya que el contrato AccessControl ya dispone de este rol por defecto.

bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00;

A partir de todas estas características que necesitas para el manejo de roles, puedes desarrollar tus contratos de la forma más conveniente.

Conclusión

Sabemos que la principal filosofía de Blockchain es la descentralización, pero en determinados casos de uso se puede requerir tareas centralizadas en pocos usuarios.

No necesariamente un contrato va a manipular grandes fondos de dinero, y si es el caso, pueden existir razonables motivos para utilizar roles y privilegios para administrar estos. Lo importante es que los usuarios del contrato sepan que esto es así y estén de acuerdo.

Por ejemplo, un contrato que guarda información de estudiantes y sus notas. Los propios estudiantes no deben ser capaz de modificar sus datos, solo un profesor debe poder hacerlo.

Implementa la administración de roles de forma inteligente para crear tus contratos y que cada tipo de cuenta tenga sus responsabilidades. El caso de uso que tengas que resolver determinará cómo debes desarrollar tu contrato.


Contribución creada por: Kevin Fiorentino (Platzi contributor).

Aportes 11

Preguntas 3

Ordenar por:

¿Quieres ver más aportes, preguntas y respuestas de la comunidad?

Por acá dejo un contrato donde pongo a prueba todo lo explicado en la clase y algunas cosas adicionales que entendí en la documentacion

También hice unos unit tests para probar los distintos casos de uso del contrato. Creo les puede interesar verlos tambien:

Pueden correr los unit tests con hardhat: npx hardhat test

Cuando el profe se refiere a la cuenta de admin por default del contrato (cuenta cero) es que el rol de admin se asigna a esta dirección:

0x0000000000000000000000000000000000000000000000000000000000000000

Me imagino que esta dirección es a la que se le asigna el rol de owner cuando renuncias a él (clase pasada Ownable.sol). Si desplegamos el contrato sin el constructor (sin la asignación inicial) va a quedar inutilizable.

En lugar de utilizar require dentro del cuerpo de la función, también se puede utilizar el modificador onlyRole(ROL_XXX).

Ejemplo:

function soloUsuario() public onlyRole(ROL_USUARIO) {
    
}

Parece ser que la diferencia entre…

_grantRole()
_setupRole()

Es que _serutRole() no requere que quien llame a la función tenga el rol de admin, pero si es necesario con _grantRole()

Definir roles suena a centralizar actividades o cosas, no sé si por ahí exista algún ejemplo para poder ver cómo los aplican en la producción.

Por otro lado, es interesante ver cómo en solidity todo es estados y más estados, se juega mucho con reglas lógicas y hay que poner atención a lo que hacemos o podríamos dejar una puerta abierta que algún usuario malicioso podría explotar.

La asignación de los roles debe hacerse en bytes32 y no con el String con el que fue definido:

ROL_ADMIN = 0x7e5b835f8ef15bb117d49cb6d0658113bc08d41d8a6edf12d3d1feb5c5875330
ROL_USUARIO = 0x575582185c1302752a00c2a38ce472577737eb5e4ab29b34daf1ccaa4c9fe08a

Si en las pruebas quieren agregar más usuarios a alguno de los roles en el constructor deben asignar ROL_ADMIN como Admin de los otros roles, de lo contrario no les dejará.

Por ejemplo, si quieren que la cuenta sender tenga el ROL_ADMIN y pueda agregar usuarios al ROL_USUARIO, pueden usar el siguiente código en el constructor:

constructor() {
	_grantRole(ROL_ADMIN, msg.sender);
	_setRoleAdmin(ROL_USUARIO, ROL_ADMIN);
}

La función agregarRol no me funcionó, este es el error que obtengo al ejecutarla:

"AccessControl: account 0x5b38da6a701c568545dcfcb03fcb875f56beddc4 is missing role 0x0000000000000000000000000000000000000000000000000000000000000000"

Para solucionarlo:

  • Eliminé la constante ROL_ADMIN.
  • Modifiqué el constructor a lo siguiente:
constructor() {
    _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
}
  • Modifiqué agregarRol a lo siguiente:
function agregarRol(bytes32 role, address account) public {
    require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "Only admin");
    _grantRole(role, account);
}

En mi caso me salió un error de:
Warning: Function state mutability can be restricted to view
–> contracts/2_Roles.sol:17:5:

Lo solucioné añadiendo al final del public la palabra view, pero no se si estará bien o no…

No es necesario crear un ROL_ADMIN ya que el contrato ya dispone de este rol por defecto.

bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00;

El contrato de Access Control

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

pragma solidity ^0.8.0;

import "./IAccessControl.sol";
import "../utils/Context.sol";
import "../utils/Strings.sol";
import "../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, _msgSender());
        _;
    }


    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, 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());
        }
    }
}