WEB3DEV

Cover image for Série Raio X Solidity: Dominando o Armazenamento — Parte 2
Paulo Gio
Paulo Gio

Posted on

Série Raio X Solidity: Dominando o Armazenamento — Parte 2

https://miro.medium.com/v2/resize:fit:640/format:webp/1*YVcyrMUdO_Wh-AacSdk6fg.png

https://www.trytoprogram.com/c-programming/c-programming-pointers/

Seção #1

Vamos aprender sobre “Ponteiros/Referências de Armazenamento” agora.

O que são eles?

Simplificando, são “ponteiros” que apontam para o próprio armazenamento de um contrato.

Bem, “apontar para o próprio armazenamento de um contrato” merece uma explicação adicional.

function setStudentData(StudentData storage studentData) public {
       // código
   }
Enter fullscreen mode Exit fullscreen mode

Os ponteiros de armazenamento são “criados” usando a palavra-chave storage ao lado do nome de uma variável na assinatura da função, como mostrado em studentData acima.

Tal função só pode ser usada em uma biblioteca Solidity e não em um contrato.

Considere o código abaixo:

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

contract StoragePointers {

   struct StudentData {
       uint256 id;
       string name;
   }

   function setStudentData(StudentData storage studentData) public {
       // código
   }
}
Enter fullscreen mode Exit fullscreen mode

Isso gera um erro de compilação:

https://miro.medium.com/v2/resize:fit:640/format:webp/1*CA9IF3lQSETBZlixjF_esQ.png

É porque, por seu design, o Solidity não permite que uma função de contrato aceite quaisquer ponteiros/referências de armazenamento como seu parâmetro.

Por outro lado, o Solidity permite, por seu design, que suas bibliotecas aceitem ponteiros de armazenamento como parâmetros em suas funções.

Portanto, o código abaixo compila.

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

library StoragePointers {

   struct StudentData {
       uint256 id;
       string name;
   }

   function setStudentData(StudentData storage studentData) public {
       // código
   }
}
Enter fullscreen mode Exit fullscreen mode

Uma discussão perspicaz sobre este aspecto da linguagem está aqui, caso você queira explorar mais sobre isso.

Lembre-se, o armazenamento/variáveis em um contrato são criados apenas na implantação do contrato, na maneira como são declarados dentro do código.

Não faz sentido criar novas variáveis de armazenamento durante uma chamada de função de contrato quando tais variáveis deveriam persistir.

Existem outros aspectos também relacionados a essa característica específica do Solidity que ainda estou explorando.

Se você encontrar quaisquer fontes credíveis/relevantes onde eu possa ler mais sobre esse assunto complicado, por favor, deixe um comentário. Obrigado.

Seção #2

Considere o código de exemplo abaixo:

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

library StoragePointers {

    struct StudentData {
        uint256 id;
        string name;
    }
    // para ler -> memória
    function getStudentData(StudentData memory studentData) public pure returns (uint256){
        return studentData.id;
    }
    // para escrever -> armazenamento
    function setStudentData(StudentData storage studentData, uint256 _id) public {
        studentData.id = _id;
    }
}

contract StorageP {

    using StoragePointers for *;

    // StoragePointers.StudentData _studentData;

    function setDataOfStudents(uint256 _id) public pure {
        StoragePointers.StudentData memory _studentData;
        _studentData.setStudentData(_id);
    }

   // function setDataOfStudents(uint256 _id) public {
   //     _studentData.setStudentData(_id);
   // }
}
Enter fullscreen mode Exit fullscreen mode

A função setStudentData() da biblioteca StoragePointers só aceita variável do tipo ponteiro/referência de armazenamento como seu argumento, com outro uint256 _id.

Portanto, a função setDataOfStudents() do contrato StorageP gera um erro de compilação quando invocada usando o tipo struct _studentData, porque o tipo struct _studentData é declarado na memória.

https://miro.medium.com/v2/resize:fit:640/format:webp/1*WsGo4VXJ7HcQhHL8d0iB5A.png

Agora, o código revisado ao descomentar a parte comentada no contrato compila. Veja abaixo:

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

library StoragePointers {

    struct StudentData {
        uint256 id;
        string name;
    }
    // para ler -> memória
    function getStudentData(StudentData memory studentData) public pure returns (uint256){
        return studentData.id;
    }
    // para escrever -> armazenamento
    function setStudentData(StudentData storage studentData, uint256 _id) public {
        studentData.id = _id;
    }
}

contract StorageP {

    using StoragePointers for *;

    StoragePointers.StudentData _studentData;

    // function setDataOfStudents(uint256 _id) public pure {
    //  StoragePointers.StudentData memory _studentData;
    //  _studentData.setStudentData(_id);
    // }

    function setDataOfStudents(uint256 _id) public {
        _studentData.setStudentData(_id);
    }
}
Enter fullscreen mode Exit fullscreen mode

Aqui, como _studentData é uma variável de armazenamento do contrato, ela pode invocar setStudentData(_id).

Aprendizado:

Uma função (de uma biblioteca) só aceitará uma referência de armazenamento como parâmetro se for declarada dessa forma e essa declaração só pode acontecer em uma biblioteca, pelo design do Solidity, e não em um contrato.

Essa biblioteca pode definitivamente ser importada/reutilizada em um contrato para alcançar essa funcionalidade dentro de um contrato.

Seção #3

Você já leu sobre Passagem por Valor vs. Passagem por Referência?

Usarei essa analogia aqui.

Simplificando, Passagem por Valor = você cria outra variável atribuindo o valor da 1ª variável. Assim, há 2 cópias (variáveis) do mesmo valor.

Mudanças em qualquer um dos valores não afetarão a outra cópia.

Passagem por Referência, por outro lado = quando atribuímos a referência (a um valor em si) a outra variável, a nova variável ainda se refere/aponta apenas para o valor original.

Isso significa que ambas as variáveis de referência apontam para o mesmo valor.

E, portanto, mudanças feitas no valor acessando qualquer uma das referências refletirão claramente na outra também.

Vou abordar os 2 cenários acima usando um código de exemplo abaixo.

Cenário #1

Atribuindo uma variável de armazenamento a uma variável local/memória em uma função que acaba criando 2 cópias diferentes de um valor.

Cenário #2

Atribuindo uma variável de armazenamento a uma variável do tipo referência de armazenamento em uma função que acaba criando outra referência ao armazenamento, o que significa duas referências apontando para o mesmo valor (não duas cópias separadas).

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

library StoragePointers {

    struct StudentData {
        uint256 id;
        string name;
    }
    // para ler -> memória
    function getStudentData(StudentData memory studentData) public pure returns (uint256){
        return studentData.id;
    }
    // para escrever -> armazenamento
    function setStudentData(StudentData storage studentData, uint256 _id) public {
        studentData.id = _id;
    }
}

contract StorageP {

    using StoragePointers for *;

    StoragePointers.StudentData _studentDataSto;

    // variável de armazenamento: mapeamento
    // uint256: 'id' aponta para um estudante específico
    mapping(uint256 => StoragePointers.StudentData) public studData;

    function setDataOfStudentsStorage(uint256 _id) public {
        _studentDataSto.setStudentData(_id);
    }
    // ======================
    // Analogia: Passagem por VALOR
    function returnMemory() public view returns (uint256) {
        StoragePointers.StudentData memory _studentDataMem;
        _studentDataMem.id = _studentDataSto.id;    // 2 cópias criadas
        return (_studentDataMem.id + 1);  
        // "+1" no `return` NÃO afeta o valor original de "id" da variável de armazenamento "_studentData"    
    }

    function returnStorage() public view returns (uint256) {
        return _studentDataSto.id;
    }
    // ======================

    // Analogia: Passagem por REFERÊNCIA
    function setDataOfStudentsUsingStorageRef(uint256 _id, string memory _name) public {
        // ambos o mapeamento com índice (id) e o newStudentData apontam para o mesmo local de armazenamento
        StoragePointers.StudentData storage newStudentData = studData[_id];
        newStudentData.id = _id;
        newStudentData.name = _name;
    }
}
Enter fullscreen mode Exit fullscreen mode

Cenário #1:

Após implantar o contrato, execute setDataOfStudentsStorage(uint256 _id) fornecendo _id = 5 como argumento de entrada.

Agora, execute ambas as funções: returnMemory() e returnStorage() uma após a outra.

Você obterá a seguinte saída:

https://miro.medium.com/v2/resize:fit:640/format:webp/1*Eq81VFRlYNgZQ0m2jOeK5g.png

Consulte os comentários escritos ao lado do código acima para melhor entendimento.

A linha de código abaixo criou 2 cópias diferentes do mesmo valor de armazenamento.

_studentDataMem.id = _studentDataSto.id;
Enter fullscreen mode Exit fullscreen mode

É por isso que return (_studentDataMem.id + 1); retornou 6 enquanto return _studentDataSto.id; retornou 5 apenas.

Cenário #2:

Execute a função setDataOfStudentsUsingStorageRef() e insira _id = 1 e _name = Manu.

Em seguida, verifique os valores usando o mapeamento studData[id = 1] => "Manu":

https://miro.medium.com/v2/resize:fit:640/format:webp/1*aA-77y2J9pzQK8qMgHK-UQ.png

A linha de código abaixo cria uma nova referência de armazenamento (newStudentData) para o mesmo valor de armazenamento que já está sendo referenciado pelo mapeamento studData[_id]; em algum local de armazenamento no armazenamento do contrato.

StoragePointers.StudentData storage newStudentData = studData[_id];
Enter fullscreen mode Exit fullscreen mode

É por isso que o mapeamento fornece a saída de id = 1 e name = Manu, pois a transação de setDataOfStudentsUsingStorageRef() fez mudanças diretamente no armazenamento do contrato.

Ufa... Eu sei, realmente, foi muita coisa para absorver.

Eu recomendo fortemente que você execute os mesmos trechos de código que criei para sua compreensão, no Remix IDE, inserindo diferentes valores para ver as saídas.

Isso conclui a parte 2.

E, como sempre, serei grato se você puder deixar um feedback honesto/reação e/ou crítica construtiva sobre este artigo para que eu possa melhorá-lo.

Na próxima e última parte, explicarei alguns conceitos avançados de armazenamento usando o Yul e transformarei você em um profissional! :)

Até a próxima vez, boa programação (em Solidity)! :)

Bibliografia:

https://docs.soliditylang.org/en/v0.8.15/internals/layout_in_storage.html?source=post_page-----84415fb11e60--------------------------------

https://betterprogramming.pub/all-about-solidity-data-locations-part-i-storage-e50604bfc1ad?source=post_page-----84415fb11e60--------------------------------

https://coinsbench.com/storage-pointers-in-solidity-4a2d7c55a054#:~:text=Photo%20by%20Jefferson%20Santos%20on,a%20specific%20value%20or%20variable

https://forum.soliditylang.org/t/storage-across-contracts/352?source=post_page-----84415fb11e60--------------------------------

Artigo original publicado por Manu Kapoor. Traduzido por Paulinho Giovannini.

Top comments (0)