DIY implementation

MEM function code and spec

This MEM function facilitates the minting and management of domain names on EVM chains. It integrates with everpay.io for minting fee payments and verifies EVM signatures for transaction authenticity.

Bare bones function with EVM auth

function.js
export async function handle(state, action) {
  const input = action.input;

  if (input.function === "mint") {
    const { caller, sig, domain, tx_fee } = input;

    _notPaused();

    const encodedMessage = btoa(`${state.sig_message}${state.counter}`);
    await _moleculeSignatureVerification(caller, encodedMessage, sig);

    const normalized_domain = _validateDomain(domain);
    const normalized_caller = caller.toLowerCase();
    const caller_index = state.domains.findIndex(
      (domain) => domain.owner === normalized_caller,
    );
    await _validatePayment(normalized_domain, tx_fee, caller);

    // assign primary domain
    if (caller_index === -1) {
      state.domains.push({
        owner: normalized_caller,
        domain: normalized_domain,
        is_primary: true,
      });
      // assign primary domain for reverse resolving via KVS
      SmartWeave.kv.put(`user_${normalized_domain}`, normalized_caller);
      SmartWeave.kv.put(normalized_caller, `user_${normalized_domain}`);

      return { state };
    }

    state.domains.push({
      owner: normalized_caller,
      domain: normalized_domain,
      is_primary: false,
    });

    return { state };
  }

  if (input.function === "setPrimaryDomain") {
    const { caller, sig, domain } = input;

    _notPaused();

    const normalized_domain = _handleDomainSyntax(domain);
    const normalized_caller = caller.toLowerCase();
    const domain_index = state.domains.findIndex(
      (user) =>
        user.domain === normalized_domain && user.owner === normalized_caller,
    );
    const old_domain_index = state.domains.findIndex(
      (user) => user.is_primary && user.owner === normalized_caller,
    );
    ContractAssert(domain_index !== -1, "ERROR_UNAUTHORIZED_CALLER");

    const encodedMessage = btoa(`${state.sig_message}${state.counter}`);
    await _moleculeSignatureVerification(caller, encodedMessage, sig);

    SmartWeave.kv.put(`user_${normalized_domain}`, normalized_caller);
    SmartWeave.kv.put(normalized_caller, `user_${normalized_domain}`);
    state.domains[domain_index].is_primary = true;

    if (old_domain_index !== -1) {
      state.domains[old_domain_index].is_primary = false;
    }

    return { state };
  }

  if (input.function === "transfer") {
    const { caller, sig, domain, target } = input;

    _notPaused();

    const normalized_domain = _handleDomainSyntax(domain);
    const normalized_caller = caller.toLowerCase();
    _validateEoaSyntax(target);
    const normalized_target = target.toLowerCase();

    const domainIndex = state.domains.findIndex(
      (user) =>
        user.domain === normalized_domain && user.owner === normalized_caller,
    );
    ContractAssert(domainIndex !== -1, "ERROR_DOMAIN_NOT_FOUND");

    const encodedMessage = btoa(`${state.sig_message}${state.counter}`);
    await _moleculeSignatureVerification(caller, encodedMessage, sig);

    if (state.domains[domainIndex].is_primary) {
      SmartWeave.kv.del(`user_${normalized_domain}`);
      SmartWeave.kv.del(normalized_caller);
      state.domains[domainIndex].is_primary = false;
    }

    state.domains[domainIndex].owner = normalized_target;

    return { state };
  }

  if (input.function === "resolve") {
    const { domain, address } = input;

    if (domain) {
      const normalized_domain = _handleDomainSyntax(domain);
      const res = SmartWeave.kv.get(normalized_domain);
      return { result: res };
    }

    const res = SmartWeave.kv.get(address.toLowerCase());
    return { result: res };
  }

  // ADMIN FUNCTIONS

  if (input.function === "pauseUnpauseContract") {
    const { sig } = input;

    const encodedMessage = btoa(`${state.sig_message}${state.counter}`);
    await _moleculeSignatureVerification(
      state.admin_address,
      encodedMessage,
      sig,
    );

    const status = state.isPaused;
    state.isPaused = !status;

    return { state };
  }

  function _notPaused() {
    ContractAssert(!state.isPaused, "ERROR_CONTRACT_PAUSED");
  }

  function _validateDomain(domain) {
    const normalized = domain.trim().toLowerCase().normalize("NFKC");
    ContractAssert(
      /^[a-z0-9]+$/.test(normalized),
      "ERROR_INVALID_DOMAIN_SYNTAX",
    );
    ContractAssert(
      !state.domains.map((user) => user.domain).includes(normalized),
      "ERROR_DOMAIN_MINTED",
    );
    return normalized;
  }

  function _handleDomainSyntax(domain) {
    const normalized = domain.trim().toLowerCase().normalize("NFKC");
    ContractAssert(
      /^[a-z0-9]+$/.test(normalized),
      "ERROR_INVALID_DOMAIN_SYNTAX",
    );
    return normalized;
  }

  function _validateEoaSyntax(address) {
    ContractAssert(
      /^(0x)?[0-9a-fA-F]{40}$/.test(address),
      "ERROR_INVALID_EOA_ADDR",
    );
  }

  async function _moleculeSignatureVerification(caller, message, signature) {
    try {
      ContractAssert(
        !state.signatures.includes(signature),
        "ERROR_SIGNATURE_ALREADY_USED",
      );

      const isValid = await EXM.deterministicFetch(
        `${state.evm_molecule_endpoint}/signer/${caller}/${message}/${signature}`,
      );
      ContractAssert(isValid.asJSON()?.result, "ERROR_UNAUTHORIZED_CALLER");
      state.signatures.push(signature);
      state.counter += 1;
    } catch (error) {
      throw new ContractError("ERROR_MOLECULE.SH_CONNECTION");
    }
  }

  function _getDomainType(domain) {
    return `l${domain.length}`;
  }

  async function _validatePayment(domain, txid, from) {
    try {
      ContractAssert(!state.payments.includes(txid), "ERROR_DOUBLE_SPENDING");

      const domainType = _getDomainType(domain);
      const cost = state.pricing[domainType];
      const req = await EXM.deterministicFetch(
        `${state.ever_molecule_endpoint}/${txid}`,
      );
      const tx = req.asJSON();
      ContractAssert(
        tx?.tokenSymbol == state.token_symbol &&
          tx?.action === "transfer" &&
          !!Number(tx?.amount) &&
          tx?.to == state.treasury_address &&
          tx?.from.toLowerCase() === from.toLowerCase(),
        "ERROR_INVALID_AR_PRICE",
      );

      ContractAssert(
        Number(tx?.amount) >= Number((cost * state.token_decimals).toFixed()),
        "ERROR_UNDERPAID",
      );

      state.payments.push(txid);
    } catch (error) {
      throw new ContractError("ERROR_MOLECULE_SERVER_ERROR");
    }
  }
}

state.json
{
  "tld": ".yourdomain",
  "token_symbol": "YOUR_TOKEN_EVERPAY_KEY",
  "token_decimals": 1e18, // change according to your token
  "treasury_address": "ANY_ADDRESS_YOU_OWN_SUPPORTED_BY_EVERYPAY",
  "admin_address": "EVM_EOA",
  "sig_message": "UNIQUE_SIG_MESSAGE_TEXT",
  "domains": [],
  "evm_molecule_endpoint": "http://evm.molecule.sh",
  "nft_molecule_endpoint": "https://molext1.com/balance/nft/arbitrum",
  "ever_molecule_endpoint": "https://molecules-exm.herokuapp.com/ever/everpay/tx",
  "counter": 0,
  "pricing": {
    "l2": 1000000,
    "l3": 500000,
    "l4": 300000,
    "l5": 250000,
    "l6": 220000,
    "l7": 200000,
    "l8": 180000,
    "l9": 160000,
    "l10": 140000,
    "l11": 120000,
    "l12": 100000,
    "l13": 80000,
    "l14": 40000,
    "l15": 20000
  },
  "signatures": [],
  "payments": [],
  "publicFunctions": {
    "mint": ["caller", "sig", "domain", "tx_fee"],
    "setPrimaryDomain": ["caller", "sig", "domain"],
    "transfer": ["caller", "sig", "domain", "target"],
    "resolve": ["domain", "address"],
    "pauseUnpauseContract": ["sig"]
  }
}

Function overview

Main Functions:

  1. mint: Allows a user to mint a new domain.

    • Verifies that the function is not paused.

    • Validates the domain syntax and ensures it hasn't been minted before.

    • Verifies the EVM signature.

    • Validates the payment for the minting fee.

    • Assigns the primary domain to the caller if it doesn't exist.

    • Adds the domain to the state.

  2. setPrimaryDomain: Allows a user to set their primary domain.

    • Verifies that the function is not paused.

    • Validates the domain syntax.

    • Ensures that the caller owns the domain.

    • Verifies the EVM signature.

    • Sets the primary domain for the caller.

  3. transfer: Allows a user to transfer a domain to another user.

    • Verifies that the function is not paused.

    • Validates the domain syntax and the target address syntax.

    • Ensures the domain exists and is owned by the caller.

    • Verifies the EVM signature.

    • Transfers the domain to the target user.

  4. pauseUnpauseContract (Admin Function): Allows the admin to pause or unpause the contract.

    • Verifies the EVM signature.

    • Toggles the contract's paused status.

Helper Functions:

  • _notPaused: Ensures the function is not paused.

  • _validateDomain: Validates the domain syntax and ensures it hasn't been minted before.

  • _handleDomainSyntax: Validates the domain syntax.

  • _validateEoaSyntax: Validates the syntax of an Ethereum address.

  • _moleculeSignatureVerification: Verifies the EVM signature.

  • _getDomainType: Determines the domain type based on its length.

  • _validatePayment: Validates the payment for the minting fee.

State Variables:

  • domains: Array that holds all minted domains with their metadata.

  • isPaused: Boolean indicating if the function is paused.

  • signatures: Array of used EVM signatures to prevent reuse.

  • counter: A counter used for generating unique messages for signature verification.

  • payments: Array of used payment transaction IDs to prevent double spending.

  • pricing: Pricing structure for domain minting based on domain type.

  • evm_molecule_endpoint: Endpoint for EVM signature verification.

  • ever_molecule_endpoint: Endpoint for payment validation.

  • token_symbol: EverPay's symbol of the accepted payment token.

  • token_decimals: Decimals of the accepted payment token.

  • treasury_address: Address where minting fees are sent.

  • admin_address: Address of the admin.

  • sig_message: a string to be used as unique-message per function.

  • publicFunctions (optional): an object that defines the functions for external applications and make the source code parsable (like ABI).

Error Handling:

The function uses ContractAssert to handle errors and ensure that conditions are met before executing functions. Specific error messages like ERROR_CONTRACT_PAUSED, ERROR_INVALID_DOMAIN_SYNTAX, and ERROR_UNAUTHORIZED_CALLER provide clarity on the nature of the error.

Last updated