From f7a23f8d4fee18bebf4a6db36bf57ee29b4d19e0 Mon Sep 17 00:00:00 2001 From: Markus Waas Date: Sat, 10 Oct 2020 17:30:42 +1300 Subject: [PATCH] add v1.0 --- README.md | 67 +++++++++++++- contracts/ERC20Permit.sol | 142 ++++++++++++++++++----------- contracts/lib/ECDSA.sol | 109 ---------------------- contracts/test/ERC20PermitMock.sol | 8 -- package.json | 5 +- test/ERC20Permit.test.js | 100 ++++++++++---------- 6 files changed, 202 insertions(+), 229 deletions(-) delete mode 100644 contracts/lib/ECDSA.sol diff --git a/README.md b/README.md index a008fcc..ae22608 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,67 @@ # ERC20-permit -Package for implementing the ERC20 permit (EIP-2612) + +Package for implementing the ERC20 permit (EIP-2612). Unaudited, use at own risk. + +## Installation + +1. Install the package via NPM: + +```bash +$ npm install @soliditylabs/erc20-permit --save-dev +``` + +Or Yarn: + +```bash +$ yarn add @soliditylabs/erc20-permit --dev +``` + +2. Import it into your ERC-20 contract: + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.7.0; + +import {ERC20, ERC20Permit} from "@soliditylabs/erc20-permit/contracts/ERC20Permit.sol"; + +contract ERC20PermitToken is ERC20Permit { + constructor (uint256 initialSupply) ERC20("ERC20Permit-Token", "EPT") { + _mint(msg.sender, initialSupply); + } + + function mint(address to, uint256 amount) public { + _mint(to, amount); + } + + function burn(address from, uint256 amount) public { + _burn(from, amount); + } +} +``` + +## Running tests + +1. Clone the repository + +```bash +$ git clone https://github.com/soliditylabs/ERC20-Permit +``` + +2. Install the dependencies + +```bash +$ cd ERC20-Permit +$ npm install +``` + +3. Run Buidler Node + +```bash +$ npx buidler node +``` + +4. Run tests + +```bash +$ npm test +``` diff --git a/contracts/ERC20Permit.sol b/contracts/ERC20Permit.sol index a5cfeda..a0f8a0e 100644 --- a/contracts/ERC20Permit.sol +++ b/contracts/ERC20Permit.sol @@ -1,10 +1,9 @@ -//SPDX-License-Identifier: MIT +// SPDX-License-Identifier: MIT pragma solidity ^0.7.0; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/utils/Counters.sol"; -import {ECDSA} from "./lib/ECDSA.sol"; import {IERC2612Permit} from "./IERC2612Permit.sol"; import "@nomiclabs/buidler/console.sol"; @@ -22,14 +21,10 @@ abstract contract ERC20Permit is ERC20, IERC2612Permit { mapping(address => Counters.Counter) private _nonces; - bytes32 private immutable _PERMIT_TYPEHASH = keccak256( - "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" - ); - // Mapping of ChainID to domain separators. This is a very gas efficient way // to not recalculate the domain separator on every call, while still // automatically detecting ChainID changes. - mapping(uint256 => bytes32) private _domainSeparators; + mapping(uint256 => bytes32) public domainSeparators; constructor() { _updateDomainSeparator(); @@ -52,59 +47,62 @@ abstract contract ERC20Permit is ERC20, IERC2612Permit { ) public virtual override { require(block.timestamp <= deadline, "ERC20Permit: expired deadline"); - bytes32 hashStruct = keccak256( - abi.encode( - _PERMIT_TYPEHASH, - owner, - spender, - amount, - _nonces[owner].current(), - deadline - ) - ); + // Assembly for more efficiently computing: + // bytes32 hashStruct = keccak256( + // abi.encode( + // _PERMIT_TYPEHASH, + // owner, + // spender, + // amount, + // _nonces[owner].current(), + // deadline + // ) + // ); + + bytes32 hashStruct; + uint256 nonce = _nonces[owner].current(); - bytes32 hash = keccak256( - abi.encodePacked(uint16(0x1901), _domainSeparator(), hashStruct) - ); + assembly { + // Load free memory pointer + let memPtr := mload(64) + + // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)") + mstore(memPtr, 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9) + mstore(add(memPtr, 32), owner) + mstore(add(memPtr, 64), spender) + mstore(add(memPtr, 96), amount) + mstore(add(memPtr, 128), nonce) + mstore(add(memPtr, 160), deadline) + + hashStruct := keccak256(memPtr, 192) + } - address signer = ECDSA.recover(hash, v, r, s); + bytes32 eip712DomainHash = _domainSeparator(); - console.log("owner", owner); - console.log("signer", signer); + // Assembly for more efficient computing: + // bytes32 hash = keccak256( + // abi.encodePacked(uint16(0x1901), eip712DomainHash, hashStruct) + // ); - require(signer == owner, "ERC20Permit: invalid signature"); + bytes32 hash; - _nonces[owner].increment(); - _approve(owner, spender, amount); - } + assembly { + // Load free memory pointer + let memPtr := mload(64) - function testSig( - address owner, - address spender, - uint256 amount, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) public view returns(address) { - require(block.timestamp <= deadline, "ERC20Permit: expired deadline"); + mstore(memPtr, 0x1901000000000000000000000000000000000000000000000000000000000000) // EIP191 header + mstore(add(memPtr, 2), eip712DomainHash) // EIP712 domain hash + mstore(add(memPtr, 34), hashStruct) // Hash of struct - bytes32 hashStruct = keccak256( - abi.encode( - _PERMIT_TYPEHASH, - owner, - spender, - amount, - _nonces[owner].current(), - deadline - ) - ); + hash := keccak256(memPtr, 66) + } - bytes32 hash = keccak256( - abi.encodePacked(uint16(0x1901), _domainSeparators[_chainID()], hashStruct) - ); + address signer = _recover(hash, v, r, s); - return ECDSA.recover(hash, v, r, s); + require(signer == owner, "ERC20Permit: invalid signature"); + + _nonces[owner].increment(); + _approve(owner, spender, amount); } /** @@ -117,26 +115,27 @@ abstract contract ERC20Permit is ERC20, IERC2612Permit { function _updateDomainSeparator() private returns (bytes32) { uint256 chainID = _chainID(); + // no need for assembly, running very rarely bytes32 newDomainSeparator = keccak256( abi.encode( keccak256( "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" ), - keccak256(bytes(name())), - keccak256(bytes("1")), // Version + keccak256(bytes(name())), // ERC-20 Name + keccak256(bytes("1")), // Version chainID, address(this) ) ); - _domainSeparators[chainID] = newDomainSeparator; + domainSeparators[chainID] = newDomainSeparator; return newDomainSeparator; } // Returns the domain separator, updating it if chainID changes function _domainSeparator() private returns (bytes32) { - bytes32 domainSeparator = _domainSeparators[_chainID()]; + bytes32 domainSeparator = domainSeparators[_chainID()]; if (domainSeparator != 0x00) { return domainSeparator; @@ -153,4 +152,37 @@ abstract contract ERC20Permit is ERC20, IERC2612Permit { return chainID; } + + function _recover( + bytes32 hash, + uint8 v, + bytes32 r, + bytes32 s + ) internal pure returns (address) { + // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature + // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines + // the valid range for s in (281): 0 < s < secp256k1n ÷ 2 + 1, and for v in (282): v ∈ {27, 28}. Most + // signatures from current libraries generate a unique signature with an s-value in the lower half order. + // + // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value + // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or + // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept + // these malleable signatures as well. + if ( + uint256(s) > + 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0 + ) { + revert("ECDSA: invalid signature 's' value"); + } + + if (v != 27 && v != 28) { + revert("ECDSA: invalid signature 'v' value"); + } + + // If the signature is valid (and not malleable), return the signer address + address signer = ecrecover(hash, v, r, s); + require(signer != address(0), "ECDSA: invalid signature"); + + return signer; + } } diff --git a/contracts/lib/ECDSA.sol b/contracts/lib/ECDSA.sol deleted file mode 100644 index 3403ac4..0000000 --- a/contracts/lib/ECDSA.sol +++ /dev/null @@ -1,109 +0,0 @@ -//SPDX-License-Identifier: MIT -pragma solidity ^0.7.0; - -/** - * @dev Elliptic Curve Digital Signature Algorithm (ECDSA) operations. - * - * These functions can be used to verify that a message was signed by the holder - * of the private keys of a given address. - */ -library ECDSA { - /** - * @dev Returns the address that signed a hashed message (`hash`) with - * `signature`. This address can then be used for verification purposes. - * - * The `ecrecover` EVM opcode allows for malleable (non-unique) signatures: - * this function rejects them by requiring the `s` value to be in the lower - * half order, and the `v` value to be either 27 or 28. - * - * IMPORTANT: `hash` _must_ be the result of a hash operation for the - * verification to be secure: it is possible to craft signatures that - * recover to arbitrary addresses for non-hashed data. A safe way to ensure - * this is by receiving a hash of the original message (which may otherwise - * be too long), and then calling {toEthSignedMessageHash} on it. - */ - function recover(bytes32 hash, bytes memory signature) - internal - pure - returns (address) - { - // Check the signature length - if (signature.length != 65) { - revert("ECDSA: invalid signature length"); - } - - // Divide the signature in r, s and v variables - bytes32 r; - bytes32 s; - uint8 v; - - // ecrecover takes the signature parameters, and the only way to get them - // currently is to use assembly. - // solhint-disable-next-line no-inline-assembly - assembly { - r := mload(add(signature, 0x20)) - s := mload(add(signature, 0x40)) - v := byte(0, mload(add(signature, 0x60))) - } - - return recover(hash, v, r, s); - } - - /** - * @dev Overload of {ECDSA-recover-bytes32-bytes-} that receives the `v`, - * `r` and `s` signature fields separately. - */ - function recover( - bytes32 hash, - uint8 v, - bytes32 r, - bytes32 s - ) internal pure returns (address) { - // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature - // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines - // the valid range for s in (281): 0 < s < secp256k1n ÷ 2 + 1, and for v in (282): v ∈ {27, 28}. Most - // signatures from current libraries generate a unique signature with an s-value in the lower half order. - // - // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value - // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or - // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept - // these malleable signatures as well. - if ( - uint256(s) > - 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0 - ) { - revert("ECDSA: invalid signature 's' value"); - } - - if (v != 27 && v != 28) { - revert("ECDSA: invalid signature 'v' value"); - } - - // If the signature is valid (and not malleable), return the signer address - address signer = ecrecover(hash, v, r, s); - require(signer != address(0), "ECDSA: invalid signature"); - - return signer; - } - - /** - * @dev Returns an Ethereum Signed Message, created from a `hash`. This - * replicates the behavior of the - * https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_sign[`eth_sign`] - * JSON-RPC method. - * - * See {recover}. - */ - function toEthSignedMessageHash(bytes32 hash) - internal - pure - returns (bytes32) - { - // 32 is the length in bytes of hash, - // enforced by the type signature above - return - keccak256( - abi.encodePacked("\x19Ethereum Signed Message:\n32", hash) - ); - } -} diff --git a/contracts/test/ERC20PermitMock.sol b/contracts/test/ERC20PermitMock.sol index 29db8ab..ec0139a 100644 --- a/contracts/test/ERC20PermitMock.sol +++ b/contracts/test/ERC20PermitMock.sol @@ -7,12 +7,4 @@ contract ERC20PermitMock is ERC20Permit { constructor (uint256 initialSupply) ERC20("ERC20Permit-Token", "EPT") { _mint(msg.sender, initialSupply); } - - function mint(address to, uint256 amount) public { - _mint(to, amount); - } - - function burn(address from, uint256 amount) public { - _burn(from, amount); - } } diff --git a/package.json b/package.json index fc28e15..554e42e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@soliditylabs/erc20-permit", - "version": "0.1.0", + "version": "1.0.0", "description": "Package for implementing the ERC20 permit (EIP-2612)", "main": "index.js", "scripts": { @@ -43,6 +43,5 @@ }, "directories": { "test": "test" - }, - "dependencies": {} + } } diff --git a/test/ERC20Permit.test.js b/test/ERC20Permit.test.js index 859a760..e762abf 100644 --- a/test/ERC20Permit.test.js +++ b/test/ERC20Permit.test.js @@ -1,6 +1,4 @@ const { expect } = require("chai"); -const { utils } = require("ethers"); -const { deployContract } = require("ethereum-waffle"); const { signERC2612Permit } = require("eth-permit"); const Common = require("ethereumjs-common"); const { Transaction } = require("ethereumjs-tx"); @@ -11,17 +9,27 @@ const ERC20PermitMock = require("../artifacts/ERC20PermitMock.json"); const deployedErc20Permit = "0x7c2C195CD6D34B8F845992d380aADB2730bB9C6F"; const web3 = new Web3("http://localhost:8545"); +// first Buidler default account +const defaultSender = "0xc783df8a850f42e7f7e57013759c285caa701eb6"; +const defaultKey = + "c5e8f61d1ab959b397eecc0a37a6517b8e67a0e7cf1f4bce5591f3ed80199122"; + +// second Buidler default account +const defaultSpender = "0xead9c93b79ae7c1591b1fb5323bd777e86e150d4"; + +const customCommon = Common.default.forCustomChain( + "mainnet", + { + name: "buidlerevm", + networkId: 31337, + chainId: 31337, + }, + "petersburg" +); + describe("ERC20Permit", () => { let erc20Permit; - const fixture = async ([senderWallet, spenderWallet]) => ({ - erc20Permit: await deployContract(senderWallet, ERC20PermitMock, [ - utils.parseEther("100"), - ]), - sender: senderWallet.address, - spender: spenderWallet.address, - }); - beforeEach(async () => { erc20Permit = new web3.eth.Contract( ERC20PermitMock.abi, @@ -29,43 +37,25 @@ describe("ERC20Permit", () => { ); }); - it("should deploy", async () => { + it("should set allowance after a permit transaction", async () => { const value = web3.utils.toWei("1", "ether"); - const sender2 = "0xc783df8a850f42e7f7e57013759c285caa701eb6"; - const spender2 = "0xead9c93b79ae7c1591b1fb5323bd777e86e150d4"; const result = await signERC2612Permit( web3.currentProvider, deployedErc20Permit, - sender2, - spender2, + defaultSender, + defaultSpender, value ); - console.log({ result }); - - const testAddress = await erc20Permit.methods - .testSig( - sender2, - spender2, - value, - result.deadline, - result.v, - result.r, - result.s - ) - .call(); - console.log({ sender2, testAddress }); - const txParams = { - nonce: await web3.eth.getTransactionCount(sender2), - gasPrice: "0x1", - gasLimit: 9500000, + nonce: await web3.eth.getTransactionCount(defaultSender), + gasLimit: 80000, to: deployedErc20Permit, data: erc20Permit.methods .permit( - sender2, - spender2, + defaultSender, + defaultSpender, value, result.deadline, result.v, @@ -75,28 +65,32 @@ describe("ERC20Permit", () => { .encodeABI(), }; const tx = new Transaction(txParams, { - common: Common.default.forCustomChain( - "mainnet", - { - name: "buidlerevm", - networkId: 31337, - chainId: 31337, - }, - "petersburg" - ), + common: customCommon, }); - tx.sign( - Buffer.from( - "c5e8f61d1ab959b397eecc0a37a6517b8e67a0e7cf1f4bce5591f3ed80199122", - "hex" - ) - ); - const serializedTx = tx.serialize(); + tx.sign(Buffer.from(defaultKey, "hex")); const receipt = await web3.eth.sendSignedTransaction( - "0x" + serializedTx.toString("hex") + "0x" + tx.serialize().toString("hex") ); - console.log({ receipt }); + + const allowance = await erc20Permit.methods + .allowance(defaultSender, defaultSpender) + .call(); expect(receipt.status).to.be.true; + expect(allowance.toString()).to.equal(value.toString()); + + txParams.nonce = txParams.nonce + 1; + const replayTx = new Transaction(txParams, { + common: customCommon, + }); + replayTx.sign(Buffer.from(defaultKey, "hex")); + + try { + await web3.eth.sendSignedTransaction( + "0x" + replayTx.serialize().toString("hex") + ); + } catch (error) { + expect(error.message).to.contain("ERC20Permit: invalid signature"); + } }); });