0.1.6 • Published 2 years ago

@grexie/signable v0.1.6

Weekly downloads
-
License
MIT
Repository
github
Last release
2 years ago

Grexie Signable

Inheritable solidity smart contract, ABIs and Node.js library to sign Web3 requests to a Signable contract.

Installing

yarn add @grexie/signable

Usage

This repository provides a solidity abstract contract that allows solidity methods to authorize transactions based on a secure signature generated from a known signer address, controlled by a backend such as Node.js on AWS.

The signer account private key can be stored in Grexie Vault or AWS Secrets Manager and consumed from AWS Lambda, EKS, ECS or EC2 and similar services from Google, etc. The suggestion is to rotate the signer account manually and regularly, and update the signer account from a signed method such as setSigner in the smart contract.

It is also possible to delegate the signer to a registry smart contract, so that all sub-contracts can use the same signer.

Each instance of the Signable contract generates its own uniq value, which cannot be changed for the lifetime of the contract. The uniq value protects against replay attacks across multiple instances of the same contract, on different chains or on the same chain. The uniq value is the keccak256 hash of block.timestamp and address(this).

To implement the ISignable interface one should create a smart contract such as the following:

pragma solidity ^0.8.0;

import '@grexie/signable/contracts/Signable.sol';

contract MySignableContract is Signable {
  address private _signer;

  constructor(address signer_) {
    _signer = signer_;
  }

  function signer() public view virtual override(ISignable) returns (address) {
    return _signer;
  }

  function setSigner(address signer_, Signature calldata signature)
    external
    verifySignature(
      abi.encode(this.setSigner.selector, signer_),
      signature
    )
  {
    _signer = signer_;
  }
}

Then to call setSigner you would instantiate the Signer class in Node.js in a secure server environment and call the Signer#sign method. Note that this needs to be authenticated such that only users you want to have access to signer rotation can access this method. You need to plan carefully access to any signed method, and access to the private key.

import ISignable from '@grexie/signable/contracts/ISignable.json';

const signer = Signer({
  keyStore: {
    async get(signerAddress: string): string {
      return MyKeyStore.get(signerAddress);
    },
  },
  address: process.env.CONTRACT_ADDRESS,
  web3,
});

const signature = await signer.sign(
  ISignable,
  'setSigner',
  userAddress,
  newSignerAddress
);

Then on the front end you can execute a gasless (user pays the gas fees) transaction for this method:

const contract = new web3.eth.Contract(ISignable, process.env.CONTRACT_ADDRESS);

await contract.methods
  .setSigner(newSignerAddress, signature)
  .send({ from: userAddress });

Likewise you can secure any other method, including those using structs. Check out Example.sol in GitHub and the signable.spec.js for details of how this works and the test coverage.

The Signer Node.js class expects verifySignature in the contract to implement all method parameters, in order, except for the Signature parameter. You can therefore place the Signable.Signature argument anywhere in the method signature, and it will simply be filtered out by the Signer class when signing. We recommend to place it at the end of the argument list though for simplicity.

For example:

struct MyStruct2 {
  string field3;
  string field4;
  string field5;
}

struct MyStruct1 {
  string field1;
  MyStruct2 field2;
}

function myMethodA(string calldata arg1, MyStruct1 calldata arg2, Signature calldata signature)
    external
    verifySignature(
      abi.encode(this.myMethodA.selector, arg1, arg2),
      signature
    )
  {
    ...
  }

The corresponding Node.js code:

const args = [
  'arg1',
  {
    field1: 'value1',
    field2: {
      field3: 'value3',
      field4: 'value4',
      field5: 'value5',
    },
  },
];

const signature = await signer.sign(
  MyContractABI,
  'myMethodA',
  userAddress,
  ...args
);

And the front end code:

const contract = new web3.eth.Contract(
  MyContractABI,
  process.env.CONTRACT_ADDRESS
);

await contract.methods
  .myMethodA(...args, signature)
  .send({ from: userAddress });

The Signer Node.js class implements provision for an optional cache such as Redis. Pass in the cache option on construction and implement the get and set methods. The cache stores the uniq value for a contract indefinately and the signer value for a contract for 1 hour. Typically you'd implement the cache interface as follows:

const signer = new Signer({
  ...
  cache: {
    async get(key: string): Promise<string> {
      const value = await redis.get(key);
      if (!value) {
        return null;
      } else {
        return JSON.parse(value);
      }
    },
    async set(key: string, value: string, ttl: number): Promise<void> {
      await redis.setex(key, ttl, JSON.stringify(value));
    }
  }
});

const onRotate = async () => {
  await redis.del(Signer.SignerCacheKey(contractAddress));
};
0.1.6

2 years ago

0.1.5

2 years ago

0.1.4

2 years ago

0.1.3

2 years ago

0.1.2

2 years ago

0.1.1

2 years ago

0.1.0

2 years ago