Skip to content
LogoLogo

Verifying signatures

Recover the signer of an ethSign signature and act on it once. The signed message includes the contract's address and a nonce, so a signature can't be replayed here or against another contract.

signatures.lithe
(* Verify an off-chain signature, then act on it once. The signer signs
   keccak256(abi.encode(contract, to, amount, nonce)) with `ethSign`, which adds the
   EIP-191 prefix "\x19Ethereum Signed Message:\n32". *)
 
import "std/abi";
import "std/address";
import "std/crypto";
import "std/evm";
 
error InvalidSignature;
error NonceUsed of uint;
 
storage is record {
  signer : address;
  used : map(uint, bool);
  credited : map(address, uint);
}
 
external function init(const signer : address) is {
  storage.signer := signer;
}
 
view function messageHash(const to : address; const amount : uint; const nonce : uint) : bytes32 is
  Crypto.keccak256(Abi.encode(Evm.self(), to, amount, nonce))
 
pure function ethSigned(const hash : bytes32) : bytes32 is {
  const prefix : bytes := "\x19Ethereum Signed Message:\n32";
} with Crypto.keccak256(Abi.encodePacked(prefix, hash))
 
external function claim(
  const to : address;
  const amount : uint;
  const nonce : uint;
  const v : uint8;
  const r : bytes32;
  const s : bytes32
)
  raises InvalidSignature | NonceUsed
is {
  assert(not storage.used[nonce], NonceUsed(nonce));
  const digest : bytes32 := ethSigned(messageHash(to, amount, nonce));
  const recovered : address := Crypto.ecrecover(digest, v, r, s);
  assert(recovered <> Address.zero and recovered = storage.signer, InvalidSignature);
  storage.used[nonce] := true;
  storage.credited[to] := storage.credited[to] + amount;
}
  • ecrecover returns Address.zero for an invalid signature, so check for it explicitly.
  • For production use, also reject high-s signatures, which are malleable.