From 71d2c1d0022ba1b15e72dd763f1e43e3a8ed7602 Mon Sep 17 00:00:00 2001 From: Papa Smurf Date: Tue, 26 Apr 2022 23:02:35 +0200 Subject: [PATCH 1/4] feat: generalised tx packing, added readme --- contracts/AuthCompatible.sol | 4 +-- package.json | 8 ++--- src/README.md | 68 ++++++++++++++++++++++++++++++++++++ src/package.json | 4 +-- src/utils/dummy.ts | 9 ----- src/utils/index.ts | 2 +- src/utils/packParameters.ts | 24 +++++++++++++ test/authCompatible.ts | 4 +-- 8 files changed, 103 insertions(+), 20 deletions(-) create mode 100644 src/README.md delete mode 100644 src/utils/dummy.ts create mode 100644 src/utils/packParameters.ts diff --git a/contracts/AuthCompatible.sol b/contracts/AuthCompatible.sol index b500648..9f14202 100644 --- a/contracts/AuthCompatible.sol +++ b/contracts/AuthCompatible.sol @@ -55,8 +55,8 @@ contract AuthCompatible { let endOfSigExp := add(startPos, 0x80) let totalInputSize := sub(calldatasize(), endOfSigExp) - // disgusting dirty putrid abomination of a detestable drivelous hack - // because for some reason byte array pointers are being assigned the same address as another causing overwrite + // disgusting dirty putrid abomination of a detestable drivelous hack because + // for some reason byte array pointers are being assigned the same address as another causing overwrite inputs := add(inputs, mul(calldatasize(), 2)) // Store expected length of total byte array as first value diff --git a/package.json b/package.json index 0f65ce6..e92a4d3 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,9 @@ "author": { "name": "papasmurf" }, + "files": [ + "contracts" + ], "devDependencies": { "@codechecks/client": "^0.1.12", "@commitlint/cli": "^16.2.1", @@ -54,9 +57,6 @@ "typechain": "^7.0.0", "typescript": "^4.6.2" }, - "files": [ - "/contracts" - ], "keywords": [ "blockchain", "ethereum", @@ -77,7 +77,7 @@ "lint:sol": "solhint --config ./.solhint.json --max-warnings 0 \"contracts/**/*.sol\"", "lint:ts": "eslint --config ./.eslintrc.yaml --ignore-path ./.eslintignore --ext .js,.ts .", "prepare": "yarn clean && env COMPILE_MODE=production yarn compile && yarn typechain", - "postinstall": "husky install", + "_postinstall": "husky install", "postpublish": "pinst --enable", "prepublishOnly": "pinst --disable", "prettier": "prettier --config ./.prettierrc.yaml --write \"**/*.{js,json,md,sol,ts}\"", diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..3fa72d9 --- /dev/null +++ b/src/README.md @@ -0,0 +1,68 @@ +# Ethereum Access Token Helpers + +Utilities for the Ethereum Access Token smart contract system. + +Use these tools to help you generate and sign your EATs. First ensure that your smart contracts follow appropriate EAT interfaces by ensuring all functions that intend to be modified with `requiresAuth` to use the following parameters prepended before your usual function parameters: + +```solidity +function yourFunction(uint8 v, bytes32 r, bytes32 s, uint256 expiry, ...) {} +``` + +where you insert your own function parameters in place of `...`. + +## Install + +Using npm: +`npm install @violetprotocol/ethereum-access-token-helpers` + +Using yarn: +`yarn add @violetprotocol/ethereum-access-token-helpers` + +## Usage + +```typescript +import { splitSignature } from "@ethersproject/bytes"; +const { + signAuthMessage, + getSignerFromMnemonic, getSignerFromPrivateKey + packParameters +} = require("@violetprotocol/ethereum-access-token-helpers/utils"); + +const INTERVAL: number = 100 // seconds +const FUNCTION_SIGNATURE = "0xabcdefgh"; +const CONTRACT: ethers.Contract = ...; +const SIGNER: ethers.Signer = ...; +const CALLER: ethers.Signer = ...; +const VERIFIER = "0x..."; // AuthVerifier contract address + +// AuthToken domain for clear namespacing +const authDomain = { + name: "Ethereum Access Token", + version: "1", + chainId: SIGNER.getChainId(), + verifyingContract: VERIFIER, +}; + +// Construct AuthToken message with relevant data +const authMessage = { + expiry: Math.floor(new Date().getTime() / 1000) + interval, + functionCall: { + functionSignature: FUNCTION_SIGNATURE, + target: CONTRACT.address.toLowerCase(), + caller: CALLER.address.toLowerCase(), + parameters: packParameters(CONTRACT.interface, "functionName", [...params]), + }, +}; + +// Sign the AuthToken using the Signer +const signature = splitSignature(await signAuthMessage(SIGNER, this.domain, authMessage)); + +// Pass all signed data to a transaction function call +await CONTRACT.functionName( + signature.v, + signature.r, + signature.s, + authMessage.expiry, + ...params +) +``` diff --git a/src/package.json b/src/package.json index 5f5d7b5..944d713 100644 --- a/src/package.json +++ b/src/package.json @@ -1,10 +1,10 @@ { "name": "@violetprotocol/ethereum-access-token-helpers", "description": "Typescript bindings and utilities for Ethereum Access Token", - "version": "0.1.0", + "version": "0.1.1", "main": "dist/index.js", "files": [ - "dist" + "dist/*" ], "scripts": { "prepare": "bash ../scripts/prepare-artifacts.sh && tsc" diff --git a/src/utils/dummy.ts b/src/utils/dummy.ts deleted file mode 100644 index 9f8b3f3..0000000 --- a/src/utils/dummy.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { assert } from "console"; - -// Parameters are hexadecimally represented, left-padded with 0 to multiples of 64-characters (32-bytes), and concatenated together -const packParameters = (address: string, amount: number): string => { - assert(address.length === 42, "address must be 40 characters long in hexadecimal"); - return `0x${address.toLowerCase().substring(2).padStart(64, "0")}${amount.toString(16).padStart(64, "0")}`; -}; - -export { packParameters }; diff --git a/src/utils/index.ts b/src/utils/index.ts index 70c90c8..0b76cc8 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,4 +1,4 @@ export { signAuthMessage } from "./signAuthMessage"; export { signMailMessage } from "./signMailMessage"; export { getSignerFromMnemonic, getSignerFromPrivateKey } from "./signer"; -export { packParameters as packDummyParameters } from "./dummy"; +export { packParameters } from "./packParameters"; diff --git a/src/utils/packParameters.ts b/src/utils/packParameters.ts new file mode 100644 index 0000000..2e0cf56 --- /dev/null +++ b/src/utils/packParameters.ts @@ -0,0 +1,24 @@ +import { hexlify } from "@ethersproject/bytes"; +import { ethers } from "ethers"; + +const packParameters = (contractInterface: ethers.utils.Interface, functionName: string, params: any[]): string => { + // detect function fragment + const functionFragment = contractInterface.getFunction(functionName); + + // check is selected function fragment complies with auth compatible function format: + // functionName(uint8 v, bytes32 r, bytes32 s, uint256 expiry, ...) + if (!isAuthCompatible(functionFragment)) throw "packParameters: specified function is not AuthCompatible"; + + // hexlify function encoding from index 4 onwards with parameters + return hexlify(contractInterface._encodeParams(functionFragment.inputs.slice(4), params)); +}; + +const isAuthCompatible = (functionFragment: ethers.utils.FunctionFragment): boolean => { + if (functionFragment.inputs[0].name != "v" || functionFragment.inputs[0].type != "uint8") return false; + if (functionFragment.inputs[1].name != "r" || functionFragment.inputs[1].type != "bytes32") return false; + if (functionFragment.inputs[2].name != "s" || functionFragment.inputs[2].type != "bytes32") return false; + if (functionFragment.inputs[3].name != "expiry" || functionFragment.inputs[3].type != "uint256") return false; + return true; +}; + +export { packParameters }; diff --git a/test/authCompatible.ts b/test/authCompatible.ts index 2e12b15..02599d5 100644 --- a/test/authCompatible.ts +++ b/test/authCompatible.ts @@ -6,7 +6,7 @@ import { AuthVerifier } from "../src/types/AuthVerifier"; import { AuthTokenStruct } from "../src/types/IAuthVerifier"; import { DummyDapp } from "../src/types/DummyDapp"; import { signAuthMessage } from "../src/utils/signAuthMessage"; -import { packParameters as packDummyParameters } from "../src/utils/dummy"; +import { packParameters } from "../src/utils/packParameters"; import { splitSignature } from "@ethersproject/bytes"; const chai = require("chai"); @@ -55,7 +55,7 @@ describe("AuthCompatible", function () { target: this.dapp.address.toLowerCase(), caller: this.signers.admin.address.toLowerCase(), // Parameters are hexadecimally represented, left-padded with 0 to multiples of 64-characters (32-bytes), and concatenated together - parameters: packDummyParameters(this.testTokenAddress, this.amount), + parameters: packParameters(this.dapp.interface, "lend", [this.testTokenAddress, this.amount]), }, }; }); From 4ed8a26edc4485afb92a85f896219fc838a8a897 Mon Sep 17 00:00:00 2001 From: Papa Smurf Date: Tue, 26 Apr 2022 23:03:22 +0200 Subject: [PATCH 2/4] chore: bumped base package version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e92a4d3..40eb21a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@violetprotocol/ethereum-access-token", "description": "Smart contracts for on-chain token-based access", - "version": "0.1.0", + "version": "0.1.1", "author": { "name": "papasmurf" }, From 5725d603c6150ae8ecd8df3a8bc6cca044dd1a87 Mon Sep 17 00:00:00 2001 From: Papa Smurf Date: Thu, 28 Apr 2022 10:31:03 +0200 Subject: [PATCH 3/4] fix: updated README, minor variable name change --- src/README.md | 15 +++++++++++---- src/utils/packParameters.ts | 10 +++++++--- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/README.md b/src/README.md index 3fa72d9..965cb01 100644 --- a/src/README.md +++ b/src/README.md @@ -21,7 +21,7 @@ Using yarn: ## Usage ```typescript -import { splitSignature } from "@ethersproject/bytes"; +const { splitSignature } = require("@ethersproject/bytes"); const { signAuthMessage, getSignerFromMnemonic, getSignerFromPrivateKey @@ -35,6 +35,9 @@ const SIGNER: ethers.Signer = ...; const CALLER: ethers.Signer = ...; const VERIFIER = "0x..."; // AuthVerifier contract address +const recipient = "0x123..."; +const amount = 1; + // AuthToken domain for clear namespacing const authDomain = { name: "Ethereum Access Token", @@ -43,19 +46,23 @@ const authDomain = { verifyingContract: VERIFIER, }; -// Construct AuthToken message with relevant data +// Construct AuthToken message with relevant data using ERC20 `transfer(address to, uint256 amount)` as the example tx +// In the Auth compatible case, the ERC20 transfer function actually looks like this: +// `transfer(uint8 v, bytes32 r, bytes32 s, uint256 expiry, address to, uint256 amount)` +// where we just augment the original function with the required parameters for auth +// the `parameters` property takes a packed, abi-encoded set of original function parameters const authMessage = { expiry: Math.floor(new Date().getTime() / 1000) + interval, functionCall: { functionSignature: FUNCTION_SIGNATURE, target: CONTRACT.address.toLowerCase(), caller: CALLER.address.toLowerCase(), - parameters: packParameters(CONTRACT.interface, "functionName", [...params]), + parameters: packParameters(CONTRACT.interface, "transfer", [recipient, amount]), }, }; // Sign the AuthToken using the Signer -const signature = splitSignature(await signAuthMessage(SIGNER, this.domain, authMessage)); +const signature = splitSignature(await signAuthMessage(SIGNER, authDomain, authMessage)); // Pass all signed data to a transaction function call await CONTRACT.functionName( diff --git a/src/utils/packParameters.ts b/src/utils/packParameters.ts index 2e0cf56..657e73f 100644 --- a/src/utils/packParameters.ts +++ b/src/utils/packParameters.ts @@ -1,11 +1,15 @@ import { hexlify } from "@ethersproject/bytes"; import { ethers } from "ethers"; -const packParameters = (contractInterface: ethers.utils.Interface, functionName: string, params: any[]): string => { +const packParameters = ( + contractInterface: ethers.utils.Interface, + functionNameOrSelector: string, + params: any[], +): string => { // detect function fragment - const functionFragment = contractInterface.getFunction(functionName); + const functionFragment = contractInterface.getFunction(functionNameOrSelector); - // check is selected function fragment complies with auth compatible function format: + // check if selected function fragment complies with auth compatible function format: // functionName(uint8 v, bytes32 r, bytes32 s, uint256 expiry, ...) if (!isAuthCompatible(functionFragment)) throw "packParameters: specified function is not AuthCompatible"; From bf387ac87f1c9d907e1e5b9ba1ea8a647327c84a Mon Sep 17 00:00:00 2001 From: Papa Smurf Date: Thu, 28 Apr 2022 10:34:26 +0200 Subject: [PATCH 4/4] fix: updated README --- src/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/README.md b/src/README.md index 965cb01..fde10b2 100644 --- a/src/README.md +++ b/src/README.md @@ -30,7 +30,7 @@ const { const INTERVAL: number = 100 // seconds const FUNCTION_SIGNATURE = "0xabcdefgh"; -const CONTRACT: ethers.Contract = ...; +const CONTRACT: ethers.Contract = ...; // for example an ERC20 token contract const SIGNER: ethers.Signer = ...; const CALLER: ethers.Signer = ...; const VERIFIER = "0x..."; // AuthVerifier contract address