Реалізація підписання сообщений (EIP-191/EIP-712) в мобільному кошельку
Підписання сообщений — це не підписання транзакції. Користувач підтверджує довільний payload: логін у dApp, офчейн ордер на DEX, дозвіл на трансфер токенів. При неправильній реалізації підпис під "безобидним" сообщенням може бути переиспользован для авторизації нежелательної дії.
EIP-191 проти EIP-712 — в чому розбіжність на практиці
EIP-191 — базовий стандарт. Personal sign додає префікс "\x19Ethereum Signed Message:\n" + len(message) перед хешуванням. Це захищає від replay-атаки: підпис під сирим сообщенням не можна переиспользувать як підпис транзакції, тому що формати різні.
keccak256("\x19Ethereum Signed Message:\n" + message.length + message)
EIP-712 — структуровані дані. Замість строки підписується типізована структура:
keccak256("\x19\x01" + domainSeparator + hashStruct(message))
domainSeparator включає chainId, verifyingContract та name — це прив'язує підпис до конкретного контракту та мережі. Підпис, зроблений для контракту A в Ethereum mainnet, не буде прийнято контрактом B або ж у тестнет.
Реалізація EIP-712 на мобільному
Складність у правильному хешуванні структур. hashStruct рекурсивна — вложені типи хешуються окремо. Типова помилка — не включити вложений тип у encodeType:
Для структури:
Mail { Person from; Person to; string contents }
Person { address wallet; string name }
typeHash для Mail повинен включати строку "Mail(Person from,Person to,string contents)Person(address wallet,string name)" — обидва типи в алфавітному порядку вложених.
У React Native використовуємо @metamask/eth-sig-util або ethers.js v6 TypedDataEncoder. На Flutter — нативний плагін або web3dart з кастомним EIP-712 хешером (готових рішень мало, придется писати самому по спецификації).
UI для підписання сообщения
Користувач повинен видіти, що саме підписує. Для EIP-712 — декодована структура з читаємими полями, не hex-строка. Мінімум:
- Ім'я dApp + домен з
domain.name - Тип операції з типів структури
- Ключові поля: адреси, суми, deadline
MetaMask показує повне дерево структури. Для мобільного UI достатньо виділити критичні поля, решта — під кнопкою "Показати детали".
Біометрія / PIN перед підписанням — обов'язкові, аналогічно транзакціям.
Верифікація на контракте
Після підписання мобільним контракт повинен це верифікувати:
function verify(address signer, Mail calldata mail, bytes calldata signature) public view returns (bool) {
bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(
keccak256("Mail(address from,address to,string contents)"),
mail.from, mail.to,
keccak256(bytes(mail.contents))
)));
return signer == ECDSA.recover(digest, signature);
}
Тестуємо сумісність: мобільна підпис → верифікація в Hardhat-тесті. Це єдиний надійний спосіб переконатися, що хеші збігаються.
Часова шкала — 2–3 дня: EIP-191 для personal sign, EIP-712 для конкретних структур, UI з декодуванням, біометричне підтвердження, тест у контракті.







