¿Qué es el minimal proxy contract?

El minimal proxy contract es un patrón de diseño de smart contracts que permite desplegar en la blockchain de forma eficiente (bajo consumo de gas) un contrato como clon de otro previamente desplegado, conocido como contrato de implementación. Como contrapartida, la ejecución de funciones en los contratos clonados consume ligeramente más gas puesto que involucra llamadas a «delegatecall» en el contrato de implementación. Otra limitación es la imposibilidad de actualizar los contratos clonados.

Es un patrón útil en caso de requerir desplegar en la blockchain la misma versión de un contrato repetidas veces, garantizando que los clones mantienen inmutable el código del contrato de implementación en que se desarrolla toda la lógica. El estado por su parte se almacena de forma independiente en cada clon.

Se trata de un estandar estable de Ethereum, el EIP-1167.

En este artículo sobre profundización en el Minimal Proxy contract se detalla toda la técnica que envuelve a este patrón de diseño desde el punto de vista de la ejecución del código en la EVM.

¿Para qué se utiliza el patrón de minimal proxy contract en la aplicación descentralizada?

La aplicación descentralizada gestiona la relación entre el promotor y el colaborador mediante un smart contract en que se definen los hitos de la tarea y las recompensas asignadas.

Se ha desarrollado un contrato de implentación con todas las funciones necesarias para gestionar la relación. Cada vez que se alcanza un nuevo acuerdo entre un promotor y un colaborador, se despliega en la blockchain un contrato clonado para hacerse cargo de la nueva tarea.

¿Cómo se ha implementado el patrón de minimal proxy contract?

Siguiendo las buenas prácticas de programación de smart contracts en Solidity se ha optado por utilizar los contratos estandarizadas de OpenZeppelin, en particular su librería Clone.sol de ‘Minimal Clones’ y el contrato base Initializable.sol de su paquete ‘Utils’.

Se siguen las siguientes reglas de diseño:

  • El contrato de implementación deriva del contrato «Initializable.sol» e incorpora un constructor en que se llama a la función «_disableInitializers» para prevenir que pueda ser reinicializado llamando a cualquiera de las funciones con el modificador «initializer». En esa situación se emitiría el error «InvalidInitialization()».
  • El contrato de implementación desarrolla una función «initialize» para inicializar los clones, ya que estos al tratarse de contratos tipo «proxy» no llaman a ningún constructor. La función está cualificada por el modificador «initializer» para prevenir llamadas duplicadas (inicializar una segunda vez el clon). En esa situación se emitiría el error «InvalidInitialization()».
  • Se desarrolla un contrato de tipo factoría para crear los clones llamando al método «clone» de la librería «Clones». Después de desplegar el clon se llama a su método «initialize». Esta llamada ejecutará un «delegatecall» sobre el contrato de implementación.

Un aspecto importante a tener en cuenta durante la inicialización de los clones es que las variables de estado de tipo immutable en el contrato de implementación son heredadas directamente en los clones. El resto de variables se deben inicializar de forma independiente, idealmente en la llamada a la función «initialize».

Veamos un ejemplo de implementación del minimal proxy contract en fragmentos de código de Solidity.

En primer lugar el contrato de implementación:

import "@openzeppelin/contracts/proxy/utils/Initializable.sol";

contract ImplementationContract is Initializable {

	//Immutable variables are inherited by cloned contracts
	uint256 internal immutable _operationFee;

	//Will be initialized at cloned contract initialization
	address public collaborator;

	constructor(uint256 operationFee)  {
        /* 
		Prevents deployed contract implementation to be reinitialized calling 
		any "initializer" function.       
        See OpenZeppelin proxy contracts documentation of Clones and Initializable 
		classes. 
		*/
        _disableInitializers();		
        _operationFee = operationFee;
    }
	
	function initialize(address collaborator_) public initializer {        
        
        collaborator = collaborator_;     
        
    }    
}

Ahora el contrato tipo factoría para crear e inicializar los clones:

import "@openzeppelin/contracts/proxy/Clones.sol";
import "./ImplementationContract.sol";

contract FactoryContract {

	//Address of deployed implementation contract
	address public implementationContract;
	
	constructor(address implementationContract_) {
		implementationContract = implementationContract_;
	}

	function createClonedContract(address collaborator) public {

		//Creates a cloned contract
		address clonedContract = Clones.clone(implementationContract); 
		
		//Initializes cloned contract
		ImplementationContract(clonedContract).initialize(collaborator);
	}
}

Conclusión: despliegue eficaz y fiable de contratos

El patrón de minimal proxy contract es muy útil cuando se trata de desplegar de forma eficiente (reducido consumo de gas) y fiable (código inmutable) múltiples instancias de un contrato a partir de una implementación conocida.

Es especialmente remarcable la inmutabilidad de los contratos creados como clones a partir del contrato de implementación (no son actualizables). Eso aumenta la transparencia y garantiza la ejecución de las funciones desarrolladas.

Siempre es posible desplegar un nuevo contrato de implentación para modificar la lógica de negocio desarrollada en los nuevos contratos clonados.


¿Has utilizado el patrón de minimal proxy contract en alguna DApp? ¿Conoces otros patrones de diseño en Solidity similares? Te animo a que compartas tus experiencias.