Getting Started
Overview
Platform Contracts
AccountsSigning LogicAttestationsToken Escrow
Internal Contracts
Voting
Attestation Kit
OverviewEnvironment VariablesAttesterRequesterWebhooks
Share Kit
OverviewDemo
Toggle Dark Mode

Signing Logic

Learn how to recover addresses from signatures


Bloom relies on the signTypedData standard described in EIP712 for many protocol interactions including allowing users to delegate transactions to a 3rd party to pay transaction costs. Signing Logic is inherited by all Bloom contracts that validate EIP712 signatures. This contract defines all of the signatures schemas for Bloom protocol signatures and implements the logic to validate signatures keep track of used signatures so actions cannot be replayed without user permission.

Bloom SignTypedData Domain

Each signature contains a domain specification so the user understands how their signature will be used. The domain specifies the dApp name, version, chain Id and the contract intended to use the signatures.

Domain
bytes32 constant EIP712DOMAIN_TYPEHASH = keccak256(
  "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
);

bytes32 DOMAIN_SEPARATOR = hash(EIP712Domain({
  name: name,
  version: version,
  chainId: chainId,
  verifyingContract: this
}));

struct EIP712Domain {
    string  name;
    string  version;
    uint256 chainId;
    address verifyingContract;
}

function hash(EIP712Domain eip712Domain) internal pure returns (bytes32) {
  return keccak256(abi.encode(
    EIP712DOMAIN_TYPEHASH,
    keccak256(bytes(eip712Domain.name)),
    keccak256(bytes(eip712Domain.version)),
    eip712Domain.chainId,
    eip712Domain.verifyingContract
  ));
}

SigningLogic

Internal Functions

recoverSigner

Recover Signer returns the address of the user who signed a message. In order to recover the signer of a signTypedData signature, the contract must know the message that was signed. SigningLogic provides functions to generate the digest for every signTypedData action in the Bloom dApp.

Interface
function recoverSigner(bytes32 _hash, bytes _sig) external pure returns (address)
Example Schema
bytes32 constant ADD_ADDRESS_TYPEHASH = keccak256(
  "AddAddress(address addressToAdd,bytes32 nonce)"
);

struct AddAddress {
    address addressToAdd;
    bytes32 nonce;
}

function hash(AddAddress request) internal pure returns (bytes32) {
  return keccak256(abi.encode(
    ADD_ADDRESS_TYPEHASH,
    request.addressToAdd,
    request.nonce
  ));
}

function generateAddAddressSchemaHash(
  address _addressToAdd,
  bytes32 _nonce
) internal view returns (bytes32) {
  return keccak256(
    abi.encodePacked(
      "",
      DOMAIN_SEPARATOR,
      hash(AddAddress(
        _addressToAdd,
        _nonce
      ))
    )
    );
}
Example Usage
// Solidity example from AccountRegistryLogic -> linkAddresses
bytes32 _signatureDigest = generateAddAddressSchemaHash(_addressToAdd, _nonce);
require(_currentAddress == recoverSigner(_signatureDigest, _linkSignature));

// Web3
export const getFormattedTypedDataAddAddress = (
  contractAddress: string,
  chainId: number,
  addressToAdd: string,
  nonce: string,
): IFormattedTypedData => {
  return {
    types: {
      EIP712Domain: [
          { name: 'name', type: 'string' },
          { name: 'version', type: 'string' },
          { name: 'chainId', type: 'uint256' },
          { name: 'verifyingContract', type: 'address' },
      ],
      AddAddress: [
        { name: 'addressToAdd', type: 'address'},
        { name: 'nonce', type: 'bytes32'},
      ]
    },
    primaryType: 'AddAddress',
    domain: {
      name: 'Bloom Account Registry',
      version: '2',
      chainId: chainId,
      verifyingContract: contractAddress,
    },
    message: {
      addressToAdd: addressToAdd,
      nonce: nonce
    }
  }
}

newAddressLinkSig = ethSigUtil.signTypedData(unclaimedPrivkey, {
  data: getFormattedTypedDataAddAddress(registryLogicAddress, 1, alice, nonce)
})

currentAddressLinkSig = ethSigUtil.signTypedData(alicePrivkey, {
  data: getFormattedTypedDataAddAddress(registryLogicAddress, 1, unclaimed, nonce)
})

burnSignatureDigest

Mark the contents of a signature and a signer address as used so it cannot be replayed. Only burns a signature if it was successfully validated and used.

Interface
mapping (bytes32 => bool) public usedSignatures;

function burnSignatureDigest(bytes32 _signatureDigest, address _sender) internal {
  bytes32 _txDataHash = keccak256(abi.encode(_signatureDigest, _sender));
  require(!usedSignatures[_txDataHash], "Signature not unique");
  usedSignatures[_txDataHash] = true;
}
Example Usage
function validateLinkSignature(
  address _currentAddress,
  address _addressToAdd,
  bytes32 _nonce,
  bytes _linkSignature
) internal {
  bytes32 _signatureDigest = generateAddAddressSchemaHash(_addressToAdd, _nonce);
  require(_currentAddress == recoverSigner(_signatureDigest, _linkSignature));
  burnSignatureDigest(_signatureDigest, _currentAddress);
}