Hay situaciones en las que desea actualizar un contrato inteligente de Solidity. Es posible que desee corregir una vulnerabilidad de software, cambiar la lógica del contrato o agregar una nueva función. Como sabe, los contratos inteligentes son inmutables y una vez que se implementan en la cadena de bloques, no se pueden cambiar. Si implementa una nueva versión del contrato, también comienza con un almacenamiento vacío. En este tutorial revisaremos varios métodos que le permitirán actualizar un contrato inteligente de Solidity. Estos métodos funcionarán en Ethereum, Binance Smart Chain, Polygon o cualquier otra cadena de bloques compatible con EVM.
Método de actualización 1: contrato de proxy mediante una llamada de delegado
Para este primer método de actualización, los usuarios interactuarán con un contrato de proxy que no contiene ninguna lógica comercial. El contrato de proxy interactuará con el contrato real para ejecutar todas las llamadas. Para que el contrato de representación interactúe con el contrato real, usaremos una llamada de delegado. Una llamada de delegado le permitirá ejecutar una función en el contexto de otro contrato.
En el caso de una actualización, el contrato de proxy todavía se utiliza. Los usuarios interactuarán con el mismo contrato de proxy y todos los datos permanecerán almacenados en el estado. Para actualizar la lógica empresarial, crearía un nuevo contrato inteligente con el que interactúe el proxy. El contrato de proxy no contiene ninguna lógica comercial. Este método separa los datos almacenados (contrato de proxy) y la lógica empresarial (contrato separado).
Contrato de proxy utilizando un flujo de proceso de llamada de delegado:
- Bob implementa smart contractV1 que contiene lógica empresarial
- Luego, implementa un contrato de proxy que está diseñado para llamar smart contractV1
- Los usuarios interactúan con el contrato de proxy y la función de respaldo llama al contrato inteligente V1
- Todos los datos se almacenan en el contrato de proxy.
- Bob quiere cambiar la funcionalidad en su contrato inteligente, por lo que implementa smart contractV2
- Actualiza su contrato de proxy para apuntar a la nueva dirección del contrato V2
Para probar un contrato de proxy mediante una llamada de delegado, realice los siguientes pasos:
- Implementar el smartContractV1 contrato a continuación
- Implemente el contrato de proxy y configure SMARTCONTRACTWITHLOGIC en la dirección smartContractV1
- Pruebe el contrato V1
- Luego implemente el smartContractV2 contrato a continuación
- Uso de la función de actualización de proxy para configurar la dirección del contrato smartContractV2
- Pruebe el contrato V2
Contrato de apoderado
pragma solidity ^0.8.6;
contract sampleProxy {
//two assembly memory slots locations
bytes32 private constant _OWNER_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;
bytes32 private constant _SMARTCONTRACTWITHLOGIC_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
constructor() {
bytes32 slot = _OWNER_SLOT;
address _admin = msg.sender;
assembly {
//allows you to store a value in storage
sstore(slot, _admin)
}
}
//address of the owner
function admin() public view returns (address owner) {
bytes32 slot = _OWNER_SLOT;
assembly {
//read a value in storage
owner := sload(slot)
}
}
//address of the contract with business logic
function SMARTCONTRACTWITHLOGIC() public view returns (address contractwithlogic) {
bytes32 slot = _SMARTCONTRACTWITHLOGIC_SLOT;
assembly {
contractwithlogic := sload(slot)
}
}
//function used to change the address of the contract containing business logic
function upgrade(address newContract) external {
//verify the sender is the admin
require(msg.sender == admin(), 'You must be an owner only');
bytes32 slot = _SMARTCONTRACTWITHLOGIC_SLOT;
assembly {
//store in memory the new address
sstore(slot, newContract)
}
}
//user calls a function that does not exist in this contract so the fallback function is called
//assembly is used
fallback() external payable {
assembly {
//get the address of the contract that contains business logic
//save address in temporary memory
let _target := sload(_SMARTCONTRACTWITHLOGIC_SLOT)
//copy the function call in memory
//first parameter is the memory slot we want to copy the function call to
//second parameter is the memory slot we want to copy from
//third parameter is the size we want to copy which is all data
calldatacopy(0x0, 0x0, calldatasize())
//forward the call to the smart contract that contains the business logic
//specify the gas, address of contract, function we want to call and size
//if the call is successful it will be stored in the bool result
let result := delegatecall(gas(), _target, 0x0, calldatasize(), 0x0, 0)
//copy the return data into memory
returndatacopy(0x0, 0x0, returndatasize())
//if the result is 0 and failed then revert
switch result case 0 {revert(0, 0)} default {return (0, returndatasize())}
}
}
}
Pruebatelo Remezclar
smartContractV1 y smartContractV2
pragma solidity ^0.8.6;
contract smartContractV1 {
uint public age;
//set
function setAge(uint newAge) external {
age = newAge;
}
}
pragma solidity ^0.8.6;
//in the upgraded contract you need to keep all existing state variables in the same order
//add new state variables below existing state variables or you will overwrite data
contract smartContractV2 {
uint public age1;
uint public age2;
//set
function setAge1(uint newAge1) external {
age1 = newAge1;
}
//set
function setAge2(uint newAge2) external {
age2 = newAge2;
}
}
Pruebatelo Remezclar
Método de actualización 2: el patrón de interfaz
Para el segundo método de actualización, un contrato inteligente utilizará una interfaz para llamar a una función en otro contrato. El objetivo es abstraer la implementación de un contrato detrás de una interfaz que solo define sus firmas de funciones. Este es un patrón bien conocido en la programación orientada a objetos, por lo que algunos desarrolladores estarán familiarizados con este concepto de abstracción.
Este contrato principal contiene la mayor parte de la lógica empresarial, pero interactúa con uno o varios contratos para realizar más funciones. Por ejemplo, un contrato inteligente que realiza un arbitraje de préstamo rápido en Uniswap y SushiSwap. Algunas de las funciones principales de los contratos dependen de otros contratos satélite para su ejecución. En cualquier momento puede implementar un nuevo contrato de satélite y actualizar su dirección en el contrato principal.
Los usuarios llaman a funciones en el contrato principal que pueden ejecutar la función en sí o interactuar con otro contrato para su ejecución. Un contrato puede interactuar con uno o varios contratos y puede utilizar una combinación de sus funciones. Esto es diferente de un contrato de proxy que utiliza una llamada de delegado porque la lógica empresarial existe en el contrato principal.
Para actualizar el contrato, no puede cambiar ninguna funcionalidad en el contrato principal, pero puede cambiar la funcionalidad en un contrato satélite. siempre que respete la interfaz. Luego actualice la dirección del contrato de satélite en el contrato principal. Este es un patrón muy popular a seguir.
El flujo de proceso del patrón de interfaz de contrato inteligente:
- Bob implementa el contrato principal que importa una interfaz definida
- El contrato principal es responsable de la lógica empresarial y el almacenamiento.
- Luego despliega un contrato satelliteV1
- Configura la dirección satelliteV1 en el contrato principal
- Los usuarios interactúan con el contrato principal.
- El contrato principal tiene funciones implementadas y llama a funciones adicionales en el contrato satelliteV1
- Bob quiere cambiar la lógica empresarial en su contrato inteligente, por lo que implementa el contrato satelliteV2
- Actualiza su contrato principal con la dirección del contrato satelliteV2
Para probar el patrón de interfaz, realice los siguientes pasos:
- Implemente el contrato principal a continuación
- Implementar el contrato satelliteV1
- Llama a mejora función del contacto principal utilizando la dirección del contrato satélite V1
- Pruebe el contrato principal llamando al getAge función
- Luego, implemente el contrato satelliteV2 a continuación
- Llama a mejora función del contacto principal utilizando la dirección del contrato satélite V2
- Pruebe el contrato principal llamando al getAge función
Contrato principal con interfaz definida
pragma solidity ^0.8.6;
//defined interface needed to interact with other contract
interface Ibusinesslogic {
function getAge() external pure returns(uint);
}
contract MainContract {
//set an admin address
address public admin;
//interface contract address
Ibusinesslogic public businesslogic;
//the admin is the owner
constructor() {
admin = msg.sender;
}
//function to upgrade the contract to point to execute function
function upgrade(address _businesslogic) external {
require(msg.sender == admin, 'only admin');
businesslogic = Ibusinesslogic(_businesslogic);
}
//call the getAge function using the businesslogic function
function getAge() external view returns(uint) {
return businesslogic.getAge();
}
}
Pruebatelo Remezclar
Contratos satélite con interfaz definida
pragma solidity ^0.8.6;
//defined interface needed to interact with other contract
interface Ibusinesslogic {
function getAge() external pure returns(uint);
}
pragma solidity ^0.8.6;
//satelliteV1 uses the Ibusinesslogic interface
contract satelliteV1 is Ibusinesslogic {
function getAge() override external pure returns(uint) {
return 25;
}
}
pragma solidity ^0.8.6;
//satelliteV2 uses the Ibusinesslogic interface
contract satelliteV2 is Ibusinesslogic {
function getAge() override external pure returns(uint) {
return 32;
}
}
Pruebatelo Remezclar
Método de actualización 3: almacene todos los datos en un contrato de almacenamiento
El tercer método de actualización es utilizar un contrato para toda la lógica empresarial y un segundo contrato para almacenar datos. Este caso de uso es valioso cuando desea actualizar un contrato inteligente y no le importa que la dirección cambie, pero desea conservar todos los datos. Los usuarios interactúan con el contrato de lógica empresarial y los datos se guardan en el contrato de almacenamiento.
Cuando actualiza, no puede cambiar ninguna de las funciones del contrato de almacenamiento, pero puede reemplazar el contrato de lógica empresarial. El contrato de lógica empresarial es responsable de toda la lógica y de interactuar con el contrato de almacenamiento obteniendo y configurando datos.
El flujo de proceso de patrón de uso de dos contratos (lógica empresarial y almacenamiento de datos):
- Bob implementa un contrato de almacenamiento de usuario
- El propósito de los contratos de almacenamiento del usuario es guardar datos
- Bob implementa el userContract
- El contrato de usuario es responsable de la lógica empresarial y envía solicitudes al contrato de almacenamiento del usuario.
- Configura la dirección userStorage en el userContract
- Configura la dirección userContract en el contrato userStorage. Esto agrega un nivel de seguridad para que solo los contratos autorizados puedan actualizar los datos.
- Los usuarios interactúan con el contrato de usuario que llama a funciones y almacena datos en el contrato de almacenamiento del usuario
- Bob quiere cambiar la funcionalidad en su userContract, por lo que implementa un nuevo usercontractV2
- Actualiza su dirección userContract en el contrato userStorage y la dirección userStorage en el userContract
Para probar este patrón utilizando dos contratos (lógica empresarial y almacenamiento de datos), realice los siguientes pasos:
- Implemente el contrato de userStorage a continuación
- Implementar el contrato de usuario a continuación
- En el contrato userStorage, llame al permitir el acceso función utilizando la dirección userContract. Esto le dará al userContract permiso para escribir en el contrato userStorage.
- En el userContract llame al setStorageContract función utilizando la dirección userStorage. Esto indica al contrato dónde obtener y configurar los datos.
- Pruebe el userContract estableciendo la variable de edad y luego obteniendo la variable de edad.
Contrato de almacenamiento de usuario
pragma solidity ^0.8.6;
//this contract is used to store data
contract UserStorage {
//a mapping to determine which contract has access to write data to this contract
//used in the modifier below
mapping(address => bool) accessAllowed;
uint private age;
//a basic mapping that allows one to set an address and a bool value
//for example - is this address registered on the platform?
mapping(address => bool) addressSet;
//function modifier checks to see if an address has permission to update data
//bool has to be true
modifier isAllowed() {
require(accessAllowed[msg.sender] == true);
_;
}
//access is allowed to the person that deployed the contract
function UserStorageAccess() public {
accessAllowed[msg.sender] = true;
}
//set an address to the accessAllowed map and set bool to true
//uses the isAllowed function modifier to determine if user can change data
//this function controls which addresses can write data to the contract
//if you update the UserContract you would add the new address here
function allowAccess (address _address) isAllowed public {
accessAllowed[_address] = true;
}
//set an address to the accessAllowed map and set bool to false
//uses the isAllowed function modifier to determine if user can change data
//this function controls which addresses need to have thier write access removed from the contract
//if you update the UserContract you would set the old contract address to false
function denyAccess (address _address) isAllowed public {
accessAllowed[_address] = false;
}
//gets an address from the addressSet map and displays true or false
function getAddressSet (address _address) public view returns(bool) {
return addressSet[_address];
}
//sets an address to the addressSet map and sets the bool true or false
//uses the isAllowed function modifier to determine if user can change data
function setAddressSet (address _address, bool _bool) isAllowed public {
addressSet[_address] = _bool;
}
//get the age from the age variable
function getAge () public view returns (uint) {
return age;
}
//set an age to the age variable
//uses the isAllowed function modifier to determine if user can change data
function setAge(uint newAge) isAllowed public {
age = newAge;
}
}
Pruebatelo Remezclar
Contrato de usuario que contiene lógica empresarial
pragma solidity ^0.8.6;
//logic is in the UserContract and data storage is in the UserStorage contract
//if we want to upgrade the usercontract we can and will not loose any data
contract UserContract {
UserStorage userStorage;
//set the address of the storage contract that this contract should user
//all functions will read and write data to this contract
function setStorageContract(address _userStorageAddress) public {
userStorage = UserStorage(_userStorageAddress);
}
//reads the addressSet map in the UserStorage contract
function isMyUserNameRegistered() public view returns(bool) {
return userStorage.getAddressSet(msg.sender);
}
//writes to the addressSet map in the UserStorage contract
function registerMe() public {
userStorage.setAddressSet(msg.sender, true);
}
//set the age in the storage contract
function setAge(uint newAge) public {
userStorage.setAge(newAge);
}
//get the age in the storage contract
function getAge() public view returns (uint){
return userStorage.getAge();
}
}
Pruebatelo Remezclar
Aunque el código de Solidity es inmutable, se puede implementar un método para evitar este concepto y tener código en múltiples contratos para tener mutabilidad. Esto permite actualizar un contrato inteligente de Solidity.