Стандарт ERC-6909 является альтернативой стандарту ERC-1155: Multi Token Standard для управления множеством токенов из одного смарт-контракта.
Основные отличия от ERC-1155:
- Интерфейс не требует реализации callback механизма для получателя токена. 
- Нет возможности делать batch вызовы, когда в одной транзакции происходит несколько операций с токенами. 
- Переработана система выдачи разрешений на использование токенов третьим лицам (апрувов). 
Интерфейс ERC-6909 представляет собой минимальную функциональность, что позволяет сократить издержки в размере кода смарт-контракта и исполнении вызова транзакций.
Важно! Любой смарт-контракт, который будет реализовывать ERC-6909, должен поддерживать ERC-165: Standard Interface Detection по умолчанию.
Интересно! В разработке стандарта принял участие Vectorized, разработчик таких проектов, как solady, ERC721A, multicaller.
Референсная имплементация
Референсная имплементация взята из спецификации ERC-6909 и упрощена мной для быстрого ознакомления. Добавлены комментарии к коду. Рекомендую изучить сначала референс, а потом читать статью дальше.
contract ERC6909 {
    /// @notice Баланс владельцев
    mapping(address owner => mapping(uint256 id => uint256 amount)) public balanceOf;
    /// @notice Выданные разрешения на использование токена третьим лицам
    mapping(address owner => mapping(address spender => mapping(uint256 id => uint256 amount))) public allowance;
    /// @notice Разрешения для операторов. Дает больше полномочий на управление всеми токенам владельца
    mapping(address owner => mapping(address spender => bool)) public isOperator;
    /// @notice Трансфер токена от имени владельца
    function transfer(address receiver, uint256 id, uint256 amount) public returns (bool) {
        if (balanceOf[msg.sender][id] < amount) revert InsufficientBalance(msg.sender, id);
        balanceOf[msg.sender][id] -= amount;
        balanceOf[receiver][id] += amount;
        emit Transfer(msg.sender, msg.sender, receiver, id, amount);
        return true;
    }
    /// @notice Трансфер токена третьими лицами, требует выданного разрешения
    function transferFrom(address sender, address receiver, uint256 id, uint256 amount) public returns (bool) {
        if (sender != msg.sender && !isOperator[sender][msg.sender]) {
            uint256 senderAllowance = allowance[sender][msg.sender][id];
            if (senderAllowance < amount) revert InsufficientPermission(msg.sender, id);
            if (senderAllowance != type(uint256).max) {
                allowance[sender][msg.sender][id] = senderAllowance - amount;
            }
        }
        if (balanceOf[sender][id] < amount) revert InsufficientBalance(sender, id);
        balanceOf[sender][id] -= amount;
        balanceOf[receiver][id] += amount;
        emit Transfer(msg.sender, sender, receiver, id, amount);
        return true;
    }
    /// @notice Выдача разрешения на передачу токена третьими лицами с ограничением количества токена
    function approve(address spender, uint256 id, uint256 amount) public returns (bool) {
        allowance[msg.sender][spender][id] = amount;
        emit Approval(msg.sender, spender, id, amount);
        return true;
    }
    /// @notice Выдача разрешения на передачу токена оператором без ограничения на количество токена
    function setOperator(address spender, bool approved) public returns (bool) {
        isOperator[msg.sender][spender] = approved;
        emit OperatorSet(msg.sender, spender, approved);
        return true;
    }
    function _mint(address receiver, uint256 id, uint256 amount) internal {
      balanceOf[receiver][id] += amount;
      emit Transfer(msg.sender, address(0), receiver, id, amount);
    }
    function _burn(address sender, uint256 id, uint256 amount) internal {
      balanceOf[sender][id] -= amount;
      emit Transfer(msg.sender, sender, address(0), id, amount);
    }
}Дальше будем детально смотреть на изменения относительно стандарта ERC-1155.
Изменение структуры хранения балансов
Структура хранения балансов - это первое на что необходимо обратить внимание. В отличие от ERC-1155 есть изменения.
// ERC-1155 из OpenZeppelin
mapping(uint256 id => mapping(address account => uint256)) private _balances;
// ERC-6909 из OpenZeppelin
mapping(address owner => mapping(uint256 id => uint256)) private _balances;Маппинг отвечающий за хранение баланса аккаунта начинается не с идентификатора, а с адреса аккаунта владельца.
Принципиально это влияет только на интерфейс взаимодействия. По прежнему, чтобы получить все балансы пользователя необходимо самостоятельно реализовывать дополнительные функции на смарт-контракте или индексировать данные off-chain. Связано это с тем, что нужно знать все идентификаторы токенов, которыми владеет аккаунт, а базовая имплементация не хранит этой информации по дефолту.
Нет обратного вызова (callback)
Согласно стандарту ERC-1155 смарт-контракт, выступающий получателем токенов должен реализовывать интерфейс ERC1155TokenReceiver.
Этот интерфейс диктует обязательную реализацию одной из функции согласно выбранному способу передачи токенов (single или batch):
-  function onERC1155Received(address _operator, address _from, uint256 _id, uint256 _value, bytes calldata _data) external returns(bytes4);
-  function onERC1155BatchReceived(address _operator, address _from, uint256[] calldata _ids, uint256[] calldata _values, bytes calldata _data) external returns(bytes4);В ERC-6909 разработчикам по прежнему можно использовать обратные вызовы, но только реализация остается на их стороне и может быть произвольной.
ERC-6909 не регламентирует механизм обратного вызова. Это позволяет экономить на размере базовой имплементации смарт-контракта и количестве операций в момент исполнения, что является эффективным с точки зрения газа и сложности.
Изменения в трансфере токенов
Аналогично обратным вызовам, стандарт поступил с batch операциями.
ERC-1155 требует реализации дополнительных функций:
- function safeBatchTransferFrom(address _from, address _to, uint256[] calldata _ids, uint256[] calldata _values, bytes calldata _data) external;
- function balanceOfBatch(address[] calldata _owners, uint256[] calldata _ids) external view returns (uint256[] memory);ERC-6909 больше не регламентирует batch операции и не требует их реализации только ради того, чтобы быть совместимым стандарту.
Batch операции могут быть добавлены по усмотрению разработчика и адаптированы под конкретные задачи проекта.
Функции трансфера токена
Трансфер максимально приближен к реализации стандарта ERC-20 c небольшой модификации.
- function transfer(address receiver, uint256 id, uint256 amount) public returns (bool);
- function transferFrom(address sender, address receiver, uint256 id, uint256 amount) public returns (bool);Sender и receiver - это привычные from и to. Добавляется id, для возможности указать идентификатор токена, который участвует в переводе. Добавление id очень напоминает ERC-721 и ERC-1155.
Гибкая система выдачи апрува
В ERС-1155 выдать апрув можно только оператору через вызов функции:
function setApprovalForAll(address _operator, bool _approved) external;В ERC-6909 вводится гибридная система выдачи апрувов. Есть две возможности выдать апрув:
- Оператору с указанием неограниченного для использования количества токенов от имени пользователя. При этом оператор может управлять токеном пользователя (с любым - id).
- Произвольному аккаунту с ограничением количества токенов, которое он сможет использовать от имени пользователя. 
Таким образом интерфейс ERC-6909 предоставляет две функции для реализации работы с апрувом:
- function setOperator(address spender, bool approved) public returns (bool);
- function approve(address spender, uint256 id, uint256 amount) public returns (bool);Подобный механизм достаточно гибок, но есть нюанс для случаев, когда аккаунту выдается апрув через обе функции. В таком случае стандарт реализует проверки в след��ющей очередности:
- Проверка на оператора. 
- Если не оператор, то проверка - allowance, выданного через вызов- approve().
Проверяется выданный апрув только при использовании функции transferFrom().
function transferFrom(address sender, address receiver, uint256 id, uint256 amount) public returns (bool) {
    // Если сендер сам отправляет токены или сендеру выданы права оператора, то тогда сразу отправить токены
    // В противном случае, скорректировать значение суммы, остающейся в распоряжение вызывающего
    if (sender != msg.sender && !isOperator[sender][msg.sender]) {
        uint256 senderAllowance = allowance[sender][msg.sender][id];
        if (senderAllowance < amount) revert InsufficientPermission(msg.sender, id);
        if (senderAllowance != type(uint256).max) {
            allowance[sender][msg.sender][id] = senderAllowance - amount;
        }
    }
  
    // Изменение балансов, отправка события
    ...
    return true;
}Таким образом, для оператора, которому выдан апрув на ограниченную сумму (через функцию approve()), не будет изменяться allowance.
Metadata токенов
При помощи стандарта ERC-6909 можно реализовывать взаимозаменяемые токены и невзаимозаменяемые одновременно. Реализация метаданных таких токенов выносится за пределы основного стандарта в отдельное расширение и является опциональной.
Почему опциональной? Ответ прост, для реализации управления LP токенами (или другими видами токенов) может быть не важен их name, symbol или URI. Именно поэтому метаданные опциональны и вынесены из базовой реализации, но использование регламентировано.
При этом, в настоящее время, только библиотека OpenZeppelin реализует метаданные как расширение к стандарту. (В solmate нет смарт-контрактов для метаданных, в solady метаданные зашиты в базовую имплементацию).
Дальше посмотрим на то, как смарт-контракты метаданных реализованы в OpenZeppelin.
Важно! На момент написания статьи, все, что касается ERC-6909 в OpenZeppelin, помечено, как draft.
ERC6909Metadata.sol
contract ERC6909Metadata {
    struct TokenMetadata {
        string name;
        string symbol;
        uint8 decimals;
    }
    mapping(uint256 id => TokenMetadata) private _tokenMetadata;
    function name(uint256 id) public view virtual returns (string memory) {
        return _tokenMetadata[id].name;
    }
    function symbol(uint256 id) public view virtual override returns (string memory) {
        return _tokenMetadata[id].symbol;
    }
    function decimals(uint256 id) public view virtual override returns (uint8) {
        return _tokenMetadata[id].decimals;
    }
}Для нас здесь интересно две вещи:
- Все функции - name(),- symbol(),- decimals()принимают один аргумент- id. Это означает, что каждый токен будет иметь собственные параметры.
- OpenZeppelin использует комбинацию - mappingи- structureдля хранения данных. Классический подход для оптимизации хранения данных.
ERC6909TokenSupply.sol
contract ERC6909TokenSupply {
    mapping(uint256 id => uint256) private _totalSupplies;
    function totalSupply(uint256 id) public view virtual override returns (uint256) {
        return _totalSupplies[id];
    }
    /// @dev Override the `_update` function to update the total supply of each token id as necessary.
    function _update(address from, address to, uint256 id, uint256 amount) internal virtual override {
      ...
    }
}Total supply аналогично name, symbol и так далее индивидуален для каждого токена.
ERC6909ContentURI.sol
contract ERC6909ContentURI is ERC6909, IERC6909ContentURI {
    string private _contractURI;
    mapping(uint256 id => string) private _tokenURIs;
    function contractURI() public view virtual override returns (string memory) {
        return _contractURI;
    }
    function tokenURI(uint256 id) public view virtual override returns (string memory) {
        return _tokenURIs[id];
    }
}Этот контракт хранит метаданные необходимые для нфт. contractURI для объявления общих данных коллекции. tokenURI для объявления индивидуальных метаданных по каждому токену.
Таким образом, используя комбинации расширений смарт-контрактов метаданных: ERC6909Metadata, ERC6909TokenSupply, ERC6909ContentURI, стандарт может одновременно управлять, как взаимозаменяемыми токенами, так и невзаимозаменяемыми.
Удаление safe именования
Соглашения об именовании safeTransfer() и safeTransferFrom() вводят в заблуждение, особенно в контексте стандартов ERC-1155 и ERC-721, так как они требуют внешних вызовов на адресе получателей (если получатель смарт-контракт). Таким образом поток выполнения передается произвольному контракту.
Согласно стандарту ERC-6909 считается, что удаление слова "safe" из всех имен функций больше не будет вводить в заблуждение и одновременно будет являться упрощением внутренней логики работы кода.
Реальное применение
В отличии от множества предлагаемых стандартов токенов, ERC-6909 был сходу опробован в Uniswap четвертой версии.
ERC-6909 выступает в качестве доказательства наличия активов у пользователя внутри протокола. Для этого основной смарт-контракт PoolManager.sol является ERC-6909 токеном.
/// @notice Holds the state for all pools
contract PoolManager is ERC6909Claims {
  ...
}Работает достаточно просто, после совершения операции (свап, удаление ликвидности) пользователь может оставить свой актив внутри протокола, а взамен получить ERC-6909. В следующий раз, для использования актива внутри протокола достаточно будет сжечь эквивалент ERC-6909.
Например, пользователь обменивает USDT на USDC. Отдает USDT, но за место получения USDC минтит себе эквивалент ERC-6909. USDC остается внутри протокола. Через некоторое время пользователь решает вернуть USDT обратно, обменяв его на USDC. Для этого ему достаточно сжечь ERC-6909 и протокол вернет ему USDT.
Таким образом, ERC-6909 позволяет существенно экономить на газе при перемещение активов. Минт ERC-6909 дешевле, чем трансфер USDC по количеству газа. Это так, потому что минт - это одна запись в сторадже смарт-контракта и одно событие Mint(), а большинство токенов добавляют дополнительные проверки при трансфере, от whitelists до другой кастомной логики.
Особенно выгодно это становится:
- пользователям, которые делает множество операций за короткий промежуток времени 
- поставщикам ликвидности, которые занимаются ребалансировкой своих позиций 
Подробнее в официальной документации Uniswap.
Вывод
Стандарт ERC-6909 это тот редкий случай, когда система упрощается, а не усложняется. За счет этого упразднения любая реализация токена проще для понимания, меньше весит и дешевле в использовании по газу.
Стандарт ERC-6909 не ��вляется обратно совместимым с ERC-1155! Это нужно понимать и учитывать. Селекторы функций разные.
При этом особый акцент я хотел бы сделать на возможности комбинировать взаимозаменяемые и невзаимозаменяемые токены. Один смарт-контракт ERC-6909 может управлять ERC-20 токенами и нфтишками легко и непринужденно.
Важно! С оговоркой, что все нфт будут реализованы в рамках одной коллекции, так как contractURI() функция не подразумевает поддержку множества коллекций.
Links
- ERC-6909 Minimal Multi-Token Standard от RareSkills 
Мы с коллегами периодически пишем в нашем Telegram-канале. Иногда это просто мысли вслух, иногда какие-то наблюдения с проектной практики. Не всегда всё оформляем в статьи, иногда проще написать пост в телегу. Так что, если интересно, что у нас в работе и что обсуждаем, можете заглянуть.
 
           
 
RetroStyle
"Отдает
USDT, но за место полученияUSDCминтит себе эквивалент ERC-6909. "Ничего не понимаю, что такое "место получения"? Бессмыслица какая-то.