Trust Score Snap: Simplifying transactions using code colors

An ETHLisbon Hackathon winner

by MetaMaskMay 25, 2023
Snaps Spotlights Feature Images (1)

MetaMask Snaps is the roadmap to making MetaMask the most extensible wallet in the world. As a developer, you can bring your features and APIs to MetaMask in totally new ways. Web3 developers are the core of this growth and this series aims to showcase the novel MetaMask Snaps being built today.

Trust Score Snap


Snap Repo: https://github.com/Tbaut/template-snap-monorepo

Why did you build it?

When it comes to transaction signing on Ethereum, the elephant in the room is that most users have very little clue about what they are signing. Decoding methods and arguments is a good first step, but if users interact with a malicious contract, the visible information can be misleading. To counterbalance this, I thought about showing quantitative data, in a very simple manner, using code colors 🟥 🟧 🟩. The idea is to show a trust score based on data that is easy to verify from multiple sources. I chose to start with 4 criterias:

  • How many users have interacted with the smart-contract overall.
  • How often the current account has interacted with this contract.
  • How old this contract is.
  • Has this contract code been verified.

Can you walk us through the technical implementation?

The insights main function


The snap is very simple, and is easy to extend. Here is the full code that we will go through.

When the insights tab is clicked on MetaMask, the onTransaction function is called. Inide it, we call each function diving us the criteria that we decided to take into account.

type ScoreResult = {
  score: number;
  description: string;
};

const onTransaction: OnTransactionHandler = async ({
  transaction,
  chainId,
}) => {
  // call all the insights criteria
  // they all return a ScoreResult
}

Those functions generally call an api, and process the results. Let’s dig into one of these to understand.

Determine a trust score based on overall interactions


Let’s dig in the function giving a score based on the total amount of transaction that a contract received. We use the Etherscan api here, but any other api could work to verify the same data.

/**
 * Gets a trust score for a contract based on the overall tx count
 * 3 -> the contract had overall >=100 txs
 * 2 -> the contract had overall 50 <= tx < 99
 * 1 -> the contract had overall tx < 50
 *
 *@param options0
 * @param options0.chainId - the chain id to call the api with
 * @param options0.contractAddress - the contract address to check
 * @returns the score and a description
 */
export async function getContractTransactionCountScore({
  chainId,
  contractAddress,
}: IContractTransactionCountScore): Promise<ScoreResult> {
  // get the api url asking for the 100th transaction
  // it will return undefined if there is none
  const url100 = getEtherscanContractTxsUrl({
    txNumber: 100,
    chainId,
    contractAddress,
  });
  const { result: result100 } = await fetchUrl<EtherscanResponse>(url100);

  // if there is a 100th tx it means the contract
  // has received at least 100 txs. We have all we need
  // we can return with the best score of 3
  if (result100.length === 1) {
    return {
      score: 3,
      description: 'more than 100 txs',
    };
  }

  // this will be call if there are less than 100 txs
  // we check if there is a 50th tx.
  const url50 = getEtherscanContractTxsUrl({
    txNumber: 50,
    chainId,
    contractAddress,
  });
  const { result: result50 } = await fetchUrl<EtherscanResponse>(url50);

  // if there is a 50th tx we can return with a score of 2
  if (result50.length === 1) {
    return {
      score: 2,
      description: 'more than 50 txs',
    };
  }

  // otherwise, return with the lowest score
  return {
    score: 1,
    description: 'fewer than 50 txs',
  };
}

Not all scores are equal


Now that we have a TrustResult for each of our criteria, it is time to apply weights. Indeed, not all of our criterias trust score should have the same weight. For instance, the amount of past transactions with the contract has a cost. If a scammer was willing to game it, it would cost them some ETH for gas. As a result, this criteria is considered more costly to game, hence more important, I gave it a weight of 3.

Similarly, the contract’s age cannot be circunvented by an attacker, they would have to deploy it, and then wait before launching their attack if they wanted to have a high trust score. I gave it a weight of 2.

The other criterias are considered weaker and have a weight of 1.

// arbitrary weight for each score
const Weights = {
  contractTransactionCountScore: 3,
  contractAgeScore: 2,
  contractVerificationScore: 1,
  contractUserTransactionScore: 1,
};

// calculate the overall trust score after applying the weights
const calculateOverallScoreWithWeight = ({
  contractTransactionCountScore,
  contractUserTransactionScore,
  contractAgeScore,
  contractVerificationScore,
}: CalculateOverallScoreWithWeightArgs) => {
  const totalWeitght = Object.values(Weights).reduce(
    (acc: number, curr: number) => acc + curr,
    0,
  );

  const overallScoreWithWeight =
    (contractUserTransactionScore.score * Weights.contractUserTransactionScore +
      contractTransactionCountScore.score *
        Weights.contractTransactionCountScore +
      contractAgeScore.score * Weights.contractAgeScore +
      contractVerificationScore.score * Weights.contractVerificationScore) /
    totalWeitght;

  return Math.floor(overallScoreWithWeight);
};

Present the final score


Finally, we now have the trust score of a smart contract, we need to show it in a simple manner. The MetaMask insights api only allows to show key value pairs. The keys are in bold, and the values have a reglar font weight. I figured that by using emojis, I could create an interface that is not intimidating to users, while still showing all the information gathered.

The function transforming the trust score into emojis:

const getColor = (result: number) => {
  switch (result) {
    case 3:
      return '🟩';
    case 2:
      return '🟧';
    default:
      return '🟥';
  }
};

And the return part of the onTransaction function, with some hacks to show key values, in a nice way:

return {
    insights: {
      '': `Trust score: ${getColor(overallScore)}`,
      ' ': '-----------------------------',
      'Contract popularity': `${getColor(
        contractTransactionCountScore.score,
      )} ${contractTransactionCountScore.description}`,
      'Previous interactions': `${getColor(
        contractUserTransactionScore.score,
      )} ${contractUserTransactionScore.description}`,
      'Contract age': `${getColor(contractAgeScore.score)} ${
        contractAgeScore.description
      }`,
      'Contract verification': `${getColor(contractVerificationScore.score)} ${
        contractVerificationScore.description
      }`,
    },
  };

Finally, I created a very simple interface to call 2 types of contracts, one very well known and used Uniswap contract, and one very recently deployed storage contract, the one presented by default on Remix. I you were to use this snap and interract with the Uniswap router contract you would see the following:

Screenshot 2023-05-25 at 6.24.47 PM

What are the next steps if you would implement this?

This is only scratching the surface of what information could give user confidence, or prevent them from being phished. The obvious first step would be to use more sources for our data. The contract verification already use Sourcify and Etherscan, but it would be better to use multiple sources for each criteria.

Then, we should apply more advanced algorithm to prevent scammers from being able to game the scores easiely. For instance, we should count the amount of unique accounts who transacted with a contract. We could do some anaysis on the accounts that interacted with it in the past as well, to make sure they are not all originating from one source. The down-side is that all this analysis is harder to make using multiple sources of truth.

There are some interesting research as well done on scam detection for smart contract that could be integrated. More info here.

All in all, the overwelming amount of positive reactions I received for this hack shows that users are demending for some more information, and I believe that the fact that it is easy to digest played a key role.

Can you tell us a little bit about yourself?

I am a software engineer at ChainSafe and I worked at Parity in the past. I love to build products with a focus on good UX. Web3 is a great playground for me because everything is so complex, trying to make it understandable to as many ppl as possible is a great challenge. I have been in the space for almost 5 years and although I have attended many hackathons, I am generally a volonteer, a mentor or a judge rather than a hacker. The main reason is that I am a perfectionist and generally don’t like quick and dirty solutions :).

When were you first introduced to MetaMask Snaps and what was your experience like?

Interestingly, my colleagues at ChainSafe have built one of the first ever snap, the one for Filecoin. I probably had a look at the code back then, but did not dig further.

At the time of hacking, the insights api wasn’t actually available to developers. I was talking to the MetaMask team in Lisbon, I did not expect to be able to add a user interface to MetaMask. They told me that it was about to be released, and that I could build on it now. In fact this was only available with a custom build, and the documentation hadn’t been released yet.

What makes MetaMask Snaps different from other wallets?

Having the ability to add custom code in a tab, while making sure that this code is safe to execute opens the doors to a huge range of enhancements. I’m very much looking forward to what will come out of this.

Tell us about what building Snaps with MetaMask is like for you?

The template monorepo made it a breath to get started. The fact that all my code could be written in one repo, with one command to launch both the test dapp and the snap, both with auto-refresh was a great developer experience.

What does Snaps mean to you?

It’s the extension to an extension. No product can fit all users. Adding this composable layer is a great opportunity for anyone asking for some feature to build it themselves.

What opportunities do you see with MetaMask Snaps and the possibilities it unlocks for the Web3 space?

I’ll talk about the insights in particular that is unlocking infinite customability depending on the needs. I bet that many more safety related insights will be developped in the future.

Any advice you would like to share with developers keen to try MetaMask Snaps?

Clone the monorepo, and enjoy a very quick start.

Building with MetaMask Snaps


To get started with MetaMask Snaps:

  1. Checkout the developer docs
  2. Install MetaMask Flask
  3. Check out a MetaMask Snaps guide
  4. Stay connected with us on Twitter, GitHub discussions, and Discord

Keep an eye out for our team at the next hackathon in an area near you! Happy BUIDLing ⚒️

Disclaimer: MetaMask Snaps are generally developed by third parties other the ConsenSys Software. Use of third-party-developed MetaMask Snaps is done at your own discretion and risk and with agreement that you will solely be responsible for any loss or damage that results from such activities. ConsenSys makes no express or implied warranty, whether oral or written, regarding any third-party-developed MetaMask Snaps and disclaims all liability for third-party developed Snaps. Use of blockchain-related software carries risks, and you assume them in full when using MetaMask Snaps.

Receive our Newsletter