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..40eb21a 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,13 @@ { "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" }, + "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..fde10b2 --- /dev/null +++ b/src/README.md @@ -0,0 +1,75 @@ +# 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 +const { splitSignature } = require("@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 = ...; // for example an ERC20 token contract +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", + version: "1", + chainId: SIGNER.getChainId(), + verifyingContract: VERIFIER, +}; + +// 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, "transfer", [recipient, amount]), + }, +}; + +// Sign the AuthToken using the Signer +const signature = splitSignature(await signAuthMessage(SIGNER, authDomain, 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..657e73f --- /dev/null +++ b/src/utils/packParameters.ts @@ -0,0 +1,28 @@ +import { hexlify } from "@ethersproject/bytes"; +import { ethers } from "ethers"; + +const packParameters = ( + contractInterface: ethers.utils.Interface, + functionNameOrSelector: string, + params: any[], +): string => { + // detect function fragment + const functionFragment = contractInterface.getFunction(functionNameOrSelector); + + // 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"; + + // 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]), }, }; });