китаев.tech

Mapping и вложенные маппинги

Как хранить значения по ключу в Solidity, использовать вложенные mapping и не пытаться перебирать их как массивы.

Mapping хранит значение по ключу

mapping — это хранилище вида ключ -> значение. В Solidity его используют для балансов, прав доступа, лимитов, разрешений и любых данных, которые нужно быстро найти по адресу или другому ключу.

Вложенный mapping добавляет второй ключ. Например: owner -> spender -> amount, где сумма зависит сразу от владельца и адреса, которому он дал разрешение.


Пишем хранилище балансов и разрешений

Создайте файл contracts/MappingVault.sol:

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

contract MappingVault {
    mapping(address => uint256) public balances;
    mapping(address => mapping(address => uint256)) public allowances;

    event Deposited(address indexed account, uint256 amount);
    event Withdrawn(address indexed account, uint256 amount);
    event Transferred(address indexed from, address indexed to, uint256 amount);
    event Approved(address indexed owner, address indexed spender, uint256 amount);

    error ZeroAmount();
    error ZeroAddress();
    error NotEnoughBalance(uint256 available, uint256 required);
    error NotEnoughAllowance(uint256 available, uint256 required);

    function deposit() external payable {
        if (msg.value == 0) {
            revert ZeroAmount();
        }

        balances[msg.sender] += msg.value;

        emit Deposited(msg.sender, msg.value);
    }

    function withdraw(uint256 amount) external {
        if (amount == 0) {
            revert ZeroAmount();
        }

        uint256 balance = balances[msg.sender];

        if (balance < amount) {
            revert NotEnoughBalance(balance, amount);
        }

        balances[msg.sender] = balance - amount;

        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "ETH transfer failed");

        emit Withdrawn(msg.sender, amount);
    }

    function transfer(address to, uint256 amount) external {
        if (to == address(0)) {
            revert ZeroAddress();
        }

        _move(msg.sender, to, amount);
    }

    function approve(address spender, uint256 amount) external {
        if (spender == address(0)) {
            revert ZeroAddress();
        }

        allowances[msg.sender][spender] = amount;

        emit Approved(msg.sender, spender, amount);
    }

    function transferFrom(address from, address to, uint256 amount) external {
        if (to == address(0)) {
            revert ZeroAddress();
        }

        uint256 allowance = allowances[from][msg.sender];

        if (allowance < amount) {
            revert NotEnoughAllowance(allowance, amount);
        }

        allowances[from][msg.sender] = allowance - amount;

        _move(from, to, amount);
    }

    function _move(address from, address to, uint256 amount) private {
        if (amount == 0) {
            revert ZeroAmount();
        }

        uint256 balance = balances[from];

        if (balance < amount) {
            revert NotEnoughBalance(balance, amount);
        }

        balances[from] = balance - amount;
        balances[to] += amount;

        emit Transferred(from, to, amount);
    }
}

Этот контракт принимает ETH, ведёт внутренний баланс каждого адреса и позволяет одному адресу дать другому право потратить часть этого баланса.


Как это работает

mapping(address => uint256) public balances — хранит число для каждого адреса. Если адрес ещё не записывали, Solidity вернёт значение по умолчанию: 0.

mapping(address => mapping(address => uint256)) public allowances — хранит значение по двум ключам. Первый ключ — владелец средств, второй ключ — адрес, которому разрешили тратить.

balances[msg.sender] += msg.value — записывает депозит в ячейку, которая принадлежит адресу отправителя.

allowances[msg.sender][spender] = amount — создаёт или перезаписывает разрешение. Здесь нет отдельного объекта allowance: Solidity вычисляет место в storage по двум ключам.

allowances[from][msg.sender] — читает разрешение для конкретной пары адресов. Другой spender получит другое значение, даже если from тот же самый.

balances[from] = balance - amount — сначала уменьшает баланс отправителя, затем контракт увеличивает баланс получателя. Такой порядок не даёт потратить больше, чем есть на внутреннем балансе.


Что важно знать про mapping

mapping нельзя перебрать циклом. Solidity не хранит список всех ключей, поэтому контракт не знает, какие адреса уже были записаны.

Ключи не удаляются физически из истории сети. delete balances[user] сбросит текущее значение в 0, но старые значения всё равно останутся в истории блокчейна.

public mapping создаёт getter автоматически. Для balances это будет balances(address), для вложенного allowancesallowances(address,address).


Частые ошибки

Пытаться получить длину mapping → у mapping нет length. Если нужен список пользователей, храните отдельный массив ключей.

Думать, что пустой ключ не существует → любой ключ существует логически и возвращает значение по умолчанию. Для uint256 это 0, для boolfalse, для addressaddress(0).

Путать порядок ключей во вложенном mappingallowances[owner][spender] и allowances[spender][owner] — разные ячейки storage.


Что дальше

Материалы китаev.tech публикуются в образовательных целях и не являются инвестиционной рекомендацией. Примеры кода и описания протоколов — для обучения; использование в продакшне на ваш собственный риск. Дисклеймер и политика конфиденциальности.