From b06c6b2abc4b91fc42a96af923d9d29f222fc94f Mon Sep 17 00:00:00 2001 From: Patrick McCorry Date: Fri, 29 May 2020 15:46:15 +0100 Subject: [PATCH] updated contract to use a single signer' --- .../account/ProxyAccountDeployer.sol | 34 +-- src/contracts/account/RelayHub.sol | 30 ++- src/contracts/account/ReplayProtection.sol | 30 +-- src/contracts/account/RevertMessage.sol | 2 +- src/contracts/account/SingleSigner.sol | 32 +++ src/contracts/ops/Echo.sol | 4 + src/contracts/ops/ReplayProtectionWrapper.sol | 10 +- src/deployment/addresses.ts | 8 +- src/index.ts | 3 + test/contracts/proxyDeployer.test.ts | 119 +++++++++- test/contracts/relayHub.test.ts | 14 +- test/contracts/replayprotection.test.ts | 222 ++++-------------- test/contracts/singleSigner.test.ts | 124 ++++++++++ 13 files changed, 393 insertions(+), 239 deletions(-) create mode 100644 src/contracts/account/SingleSigner.sol create mode 100644 test/contracts/singleSigner.test.ts diff --git a/src/contracts/account/ProxyAccountDeployer.sol b/src/contracts/account/ProxyAccountDeployer.sol index c61f39f..f2d1e43 100644 --- a/src/contracts/account/ProxyAccountDeployer.sol +++ b/src/contracts/account/ProxyAccountDeployer.sol @@ -1,15 +1,17 @@ pragma solidity 0.6.2; pragma experimental ABIEncoderV2; +import "@openzeppelin/contracts/cryptography/ECDSA.sol"; import "./ReplayProtection.sol"; import "../ops/BatchInternal.sol"; +import "../SingleSigner.sol"; /** * We deploy a new contract to bypass the msg.sender problem. */ -contract ProxyAccount is ReplayProtection, BatchInternal { +contract ProxyAccount is SingleSigner, ReplayProtection, BatchInternal { - address public owner; + event MetaTxInfo(bytes replayProtection, address replayProtectionAuthority, bytes32 indexed txid); struct MetaTx { address to; @@ -18,14 +20,6 @@ contract ProxyAccount is ReplayProtection, BatchInternal { CallType callType; } - /** - * Due to create clone, we need to use an init() method. - */ - function init(address _owner) public { - require(owner == address(0), "Owner is already set"); - owner = _owner; - } - /** * We check the signature has authorised the call before executing it. * @param _metaTx A single meta-transaction that includes to, value and data @@ -40,10 +34,16 @@ contract ProxyAccount is ReplayProtection, BatchInternal { bytes memory _signature) public returns (bool, bytes memory) { // Assumes that ProxyAccountDeployer is ReplayProtection. - bytes memory encodedData = abi.encode(_metaTx.callType, _metaTx.to, _metaTx.value, _metaTx.data); + bytes memory encodedCallData = abi.encode(_metaTx.callType, _metaTx.to, _metaTx.value, _metaTx.data); + bytes memory encodedTxData = abi.encode(encodedCallData, _replayProtection, _replayProtectionAuthority, address(this), getChainID()); + bytes32 txid = keccak256(encodedTxData); + + // Reverts if fails. + // Signer/owner is derived from SingleSigner + authenticate(txid, _signature); + replayProtection(getOwner(), _replayProtection, _replayProtectionAuthority); + emit MetaTxInfo(_replayProtection, _replayProtectionAuthority, txid); - // // Reverts if fails. - require(owner == verify(encodedData, _replayProtection, _replayProtectionAuthority, _signature), "Owner did not sign this meta-transaction."); require(_metaTx.callType == CallType.CALL || _metaTx.callType == CallType.DELEGATE, "Signer did not pick a valid calltype"); bool success; @@ -80,10 +80,14 @@ contract ProxyAccount is ReplayProtection, BatchInternal { address _replayProtectionAuthority, bytes memory _signature) public { - bytes memory encodedData = abi.encode(CallType.BATCH, _metaTxList); + bytes memory encodedCallData = abi.encode(CallType.BATCH, _metaTxList); + bytes memory encodedTxData = abi.encode(encodedCallData, _replayProtection, _replayProtectionAuthority, address(this), getChainID()); + bytes32 txid = keccak256(encodedTxData); // Reverts if fails. - require(owner == verify(encodedData, _replayProtection, _replayProtectionAuthority, _signature), "Owner did not sign this meta-transaction."); + authenticate(txid, _signature); + replayProtection(getOwner(), _replayProtection, _replayProtectionAuthority); + emit MetaTxInfo(_replayProtection, _replayProtectionAuthority, txid); // Runs the batch function in MultiSend. // It supports CALL and DELEGATECALL. diff --git a/src/contracts/account/RelayHub.sol b/src/contracts/account/RelayHub.sol index 7dfaf3c..208ac6e 100644 --- a/src/contracts/account/RelayHub.sol +++ b/src/contracts/account/RelayHub.sol @@ -1,6 +1,7 @@ pragma solidity 0.6.2; pragma experimental ABIEncoderV2; +import "@openzeppelin/contracts/cryptography/ECDSA.sol"; import "@openzeppelin/contracts/utils/Create2.sol"; import "./ReplayProtection.sol"; import "./CallTypes.sol"; @@ -25,11 +26,27 @@ contract RelayHub is ReplayProtection, CallTypes, RevertMessage { bool revertOnFail; } + /** + * Compute the signed message and fetch the signer's address. + * @param _encodedCallData Encoded call data + * @param _replayProtection Encoded Replay Protection + * @param _replayProtectionAuthority Identify the Replay protection, default is address(0) + * @param _signature Signature from signer + */ + function getSigner(bytes memory _encodedCallData, bytes memory _replayProtection, address _replayProtectionAuthority, + bytes memory _signature) public view returns(address) { + bytes memory encodedTxData = abi.encode(_encodedCallData, _replayProtection, _replayProtectionAuthority, address(this), getChainID()); + bytes32 txid = keccak256(encodedTxData); + address signer = ECDSA.recover(ECDSA.toEthSignedMessageHash(txid), _signature); + return signer; + } + /** * Each signer has a contract account (signers address => contract address). * We check the signer has authorised the target contract and function call. Then, we pass it to the * signer's contract account to perform the final execution (to help us bypass msg.sender problem). * @param _metaTx A single meta-transaction that includes to, value and data + * @param _replayProtection Encoded Replay Protection * @param _replayProtectionAuthority Identify the Replay protection, default is address(0) * @param _signer Signer's address * @param _signature Signature from signer @@ -41,11 +58,13 @@ contract RelayHub is ReplayProtection, CallTypes, RevertMessage { address _signer, bytes memory _signature) public returns(bool, bytes memory){ - bytes memory encodedData = abi.encode(CallType.CALL, _metaTx.to, _metaTx.data); + // Verify the signature + bytes memory encodedCallData = abi.encode(CallType.CALL, _metaTx.to, _metaTx.data); - // // Reverts if fails. - require(_signer == verify(encodedData, _replayProtection, _replayProtectionAuthority, _signature), + // Reverts if fails. + require(_signer == getSigner(encodedCallData, _replayProtection, _replayProtectionAuthority, _signature), "Signer did not sign this meta-transaction."); + replayProtection(_signer, _replayProtection, _replayProtectionAuthority); // Does not revert. Lets us save the replay protection if it fails. (bool success, bytes memory returnData) = _metaTx.to.call(abi.encodePacked(_metaTx.data, _signer)); @@ -71,10 +90,11 @@ contract RelayHub is ReplayProtection, CallTypes, RevertMessage { address _replayProtectionAuthority, address _signer, bytes memory _signature) public { - bytes memory encodedData = abi.encode(CallType.BATCH, _metaTxList); + bytes memory encodedCallData = abi.encode(CallType.BATCH, _metaTxList); // Reverts if fails. - require(_signer == verify(encodedData, _replayProtection, _replayProtectionAuthority, _signature), "Owner did not sign this meta-transaction."); + require(_signer == getSigner(encodedCallData, _replayProtection, _replayProtectionAuthority, _signature), + "Owner did not sign this meta-transaction."); // Go through each revertable meta transaction and/or meta-deployment. for(uint i=0; i<_metaTxList.length; i++) { diff --git a/src/contracts/account/ReplayProtection.sol b/src/contracts/account/ReplayProtection.sol index d5e34bc..5c4296e 100644 --- a/src/contracts/account/ReplayProtection.sol +++ b/src/contracts/account/ReplayProtection.sol @@ -1,7 +1,6 @@ pragma solidity 0.6.2; pragma experimental ABIEncoderV2; -import "@openzeppelin/contracts/cryptography/ECDSA.sol"; import "./IReplayProtectionAuthority.sol"; contract ReplayProtection { @@ -28,34 +27,24 @@ contract ReplayProtection { * * Why is there no signing authority? An attacker can supply an address that returns a fixed signer * so we need to restrict it to a "pre-approved" list of authorities (DAO). - * @param _callData Function name and data to be called + * @param _signer Address of the signer * @param _replayProtectionAuthority What replay protection will we check? * @param _replayProtection Encoded replay protection - * @param _signature Signer's signature */ - function verify(bytes memory _callData, + function replayProtection(address _signer, bytes memory _replayProtection, - address _replayProtectionAuthority, - bytes memory _signature) internal returns(address){ - - // Extract signer's address. - bytes memory encodedData = abi.encode(_callData, _replayProtection, _replayProtectionAuthority, address(this), getChainID()); - bytes32 txid = keccak256(encodedData); - address signer = ECDSA.recover(ECDSA.toEthSignedMessageHash(txid), _signature); + address _replayProtectionAuthority) internal { // Check the user's replay protection. if(_replayProtectionAuthority == multiNonceAddress) { // Assumes authority returns true or false. It may also revert. - require(nonce(signer, _replayProtection), "Multinonce replay protection failed"); + require(nonce(_signer, _replayProtection), "Multinonce replay protection failed"); } else if (_replayProtectionAuthority == bitFlipAddress) { - require(bitflip(signer, _replayProtection), "Bitflip replay protection failed"); + require(bitflip(_signer, _replayProtection), "Bitflip replay protection failed"); } else { - require(IReplayProtectionAuthority(_replayProtectionAuthority).updateFor(signer, _replayProtection), "Replay protection from authority failed"); + // The final "else" ensures this require() is always hit and reverts if its bad. + require(IReplayProtectionAuthority(_replayProtectionAuthority).updateFor(_signer, _replayProtection), "Replay protection from authority failed"); } - - emit ReplayProtectionInfo(_replayProtectionAuthority, _replayProtection, txid); - - return signer; } /** @@ -63,7 +52,8 @@ contract ReplayProtection { * Explained: https://github.com/PISAresearch/metamask-comp#multinonce * Allows a user to send N queues of transactions, but transactions in each queue are accepted in order. * If queue==0, then it is a single queue (e.g. NONCE replay protection) - * @param _replayProtection Contains a single nonce + * @param _signer Signer's address + * @param _replayProtection Contains the two nonces */ function nonce(address _signer, bytes memory _replayProtection) internal returns(bool) { uint256 queue; @@ -85,6 +75,8 @@ contract ReplayProtection { * Bitflip Replay Protection * Explained: https://github.com/PISAresearch/metamask-comp#bitflip * Signer flips a bit for every new transaction. Each queue supports 256 bit flips. + * @param _signer Signer's address + * @param _replayProtection Contains the two nonces */ function bitflip(address _signer, bytes memory _replayProtection) internal returns(bool) { (uint256 queue, uint256 bitsToFlip) = abi.decode(_replayProtection, (uint256, uint256)); diff --git a/src/contracts/account/RevertMessage.sol b/src/contracts/account/RevertMessage.sol index 0acdb4b..32d774d 100644 --- a/src/contracts/account/RevertMessage.sol +++ b/src/contracts/account/RevertMessage.sol @@ -2,7 +2,7 @@ pragma solidity 0.6.2; pragma experimental ABIEncoderV2; /** - * Common CALL functionality for the proxy contract and relayhub + * Extracts the revert message if a call fails. */ contract RevertMessage { diff --git a/src/contracts/account/SingleSigner.sol b/src/contracts/account/SingleSigner.sol new file mode 100644 index 0000000..47d809e --- /dev/null +++ b/src/contracts/account/SingleSigner.sol @@ -0,0 +1,32 @@ +pragma solidity 0.6.2; +pragma experimental ABIEncoderV2; + +import "@openzeppelin/contracts/cryptography/ECDSA.sol"; + +/** + * Authenticates a single signer + */ +contract SingleSigner { + + address public owner; + + /// @dev Due to create clone, we need to use an init() method. + function init(address _owner) public { + require(owner == address(0), "Owner is already set"); + owner = _owner; + } + + /// @dev Authenticates the user's signature + /// @param _txid Hash of meta-tx + /// @param _signature Signature of hash + function authenticate(bytes32 _txid, bytes memory _signature) public view { + address signer = ECDSA.recover(ECDSA.toEthSignedMessageHash(_txid), _signature); + require(signer == owner, "Owner of the proxy account did not authorise the tx"); + } + + /// @dev Return owner. + function getOwner() internal view returns (address) { + return owner; + } + +} diff --git a/src/contracts/ops/Echo.sol b/src/contracts/ops/Echo.sol index 939bc32..10c2f35 100644 --- a/src/contracts/ops/Echo.sol +++ b/src/contracts/ops/Echo.sol @@ -10,4 +10,8 @@ contract Echo { emit Broadcast(_message); } + function onlyBroadcast(string memory _message) public { + emit Broadcast(_message); + } + } \ No newline at end of file diff --git a/src/contracts/ops/ReplayProtectionWrapper.sol b/src/contracts/ops/ReplayProtectionWrapper.sol index 196808a..7aa1204 100644 --- a/src/contracts/ops/ReplayProtectionWrapper.sol +++ b/src/contracts/ops/ReplayProtectionWrapper.sol @@ -11,13 +11,13 @@ contract ReplayProtectionWrapper is ReplayProtection { /** * Easy wrapper to access ReplayProtection.verify(), an internal method. */ - function verifyPublic(bytes memory _callData, + function replayProtectionPublic( + address _signer, bytes memory _replayProtection, - address _replayProtectionAuthority, - address signer, - bytes memory _signature) public { + address _replayProtectionAuthority + ) public { - require(signer == verify(_callData, _replayProtection, _replayProtectionAuthority, _signature), "Not expected signer"); + replayProtection(_signer, _replayProtection, _replayProtectionAuthority); } function noncePublic(address _signer, bytes memory _replayProtection) public { diff --git a/src/deployment/addresses.ts b/src/deployment/addresses.ts index a791497..80e744b 100644 --- a/src/deployment/addresses.ts +++ b/src/deployment/addresses.ts @@ -8,10 +8,10 @@ export const DELEGATE_DEPLOYER_SALT_STRING = "GLOBAL_DEPLOYER"; // addresses for the salts above // IF YOU'RE CHANGING THESE THINK ABOUT DOWNSTREAM SYSTEMS export const PROXY_ACCOUNT_DEPLOYER_ADDRESS = - "0x902059Ee36702DAbdf44e7EBd760f9a9fEaE668F"; + "0x302b402DB8766A9feb8a9661E05693a05e6bd7fA"; export const BASE_ACCOUNT_ADDRESS = - "0x92085d7d57C9A261cDA2B0d4F016EB050d89F8d7"; -export const RELAY_HUB_ADDRESS = "0x27cfA763272f561CcaE1A4d17800d0Db78F91719"; -export const MULTI_SEND_ADDRESS = "0x3802FEf4c0967d908476d4452c5e25337e40CEFF"; + "0xfeD75Ff3D1067A228cbd3a373a3e6FE8693f6E82"; +export const RELAY_HUB_ADDRESS = "0x66fB0dF80DF228c5E8388777f403D9B940116a29"; +export const MULTI_SEND_ADDRESS = "0x55a4ACF2638dC588DAC38264b84393a15644e154"; export const DELEGATE_DEPLOYER_ADDRESS = "0xf0B36554191c92f3fE9f6E25cEd4FaD08c969E38"; diff --git a/src/index.ts b/src/index.ts index 7b1e8f8..4de55d7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -57,6 +57,9 @@ export { CallWrapperFactory } from "./typedContracts/CallWrapperFactory"; export { RevertMessageTester } from "./typedContracts/RevertMessageTester"; export { RevertMessageTesterFactory } from "./typedContracts/RevertMessageTesterFactory"; +export { SingleSigner } from "./typedContracts/SingleSigner"; +export { SingleSignerFactory } from "./typedContracts/SingleSignerFactory"; + export { MultiSender } from "./ts/batch/MultiSend"; export { deployMetaTxContracts } from "./deployment/deploy"; diff --git a/test/contracts/proxyDeployer.test.ts b/test/contracts/proxyDeployer.test.ts index 6da3d8d..96c1967 100644 --- a/test/contracts/proxyDeployer.test.ts +++ b/test/contracts/proxyDeployer.test.ts @@ -38,6 +38,7 @@ import { ForwardParams, CallType } from "../../src/ts/forwarders/forwarder"; import { Create2Options, getCreate2Address } from "ethers/utils/address"; import { ethers } from "ethers"; import { abi } from "../../src/typedContracts/ProxyAccount.json"; +import { Signer } from "crypto"; const expect = chai.expect; let hubClass: ProxyAccountDeployer; let accountClass: ProxyAccount; @@ -291,6 +292,105 @@ describe("ProxyAccountDeployer", () => { } ); + fnIt( + (a) => a.forward, + "catches the MetaTxInfo.", + async () => { + const { proxyDeployer, owner, sender, msgSenderCon } = await loadFixture( + createProxyAccountDeployer + ); + + const msgSenderCall = msgSenderCon.interface.functions.test.encode([]); + const forwarder = await createForwarder( + proxyDeployer, + owner, + ReplayProtectionType.MULTINONCE + ); + + await proxyDeployer.connect(sender).createProxyAccount(owner.address); + // @ts-ignore + const params = await forwarder.signMetaTransaction({ + to: msgSenderCon.address, + value: new BigNumber("0"), + data: msgSenderCall, + callType: CallType.CALL, + }); + + // @ts-ignore + const minimalTx = await forwarder.encodeSignedMetaTransaction(params); + const proxyAccount = new ProxyAccountFactory(owner).attach( + forwarder.address + ); + const txid = keccak256( + defaultAbiCoder.encode( + ["bytes", "bytes", "address", "address", "uint"], + [ + defaultAbiCoder.encode( + ["uint", "address", "uint", "bytes"], + [CallType.CALL, msgSenderCon.address, 0, msgSenderCall] + ), + params.replayProtection, + params.replayProtectionAuthority, + params.to, + params.chainId, + ] + ) + ); + const tx = sender.sendTransaction({ + to: minimalTx.to, + data: minimalTx.data, + }); + + await expect(tx) + .to.emit(proxyAccount, proxyAccount.interface.events.MetaTxInfo.name) + .withArgs( + params.replayProtection, + params.replayProtectionAuthority, + txid + ); + } + ); + + fnIt( + (a) => a.forward, + "change the replay protection authority and it should fail.", + async () => { + const { proxyDeployer, owner, sender, msgSenderCon } = await loadFixture( + createProxyAccountDeployer + ); + + const msgSenderCall = msgSenderCon.interface.functions.test.encode([]); + const forwarder = await createForwarder( + proxyDeployer, + owner, + ReplayProtectionType.MULTINONCE + ); + + await proxyDeployer.connect(sender).createProxyAccount(owner.address); + // @ts-ignore + const params = await forwarder.signMetaTransaction({ + to: msgSenderCon.address, + value: new BigNumber("0"), + data: msgSenderCall, + callType: CallType.CALL, + }); + + params.replayProtectionAuthority = + "0x0000000000000000000000000000000000000001"; + + // @ts-ignore + const minimalTx = await forwarder.encodeSignedMetaTransaction(params); + const tx = sender.sendTransaction({ + to: minimalTx.to, + data: minimalTx.data, + }); + + await expect(tx).to.be.revertedWith( + "Owner of the proxy account did not authorise the tx" + ); + } + ); + const createForwarder = async ( proxyDeployer: ProxyAccountDeployer, user: ethers.Wallet, @@ -619,7 +719,7 @@ describe("ProxyAccountDeployer", () => { ); await expect(tx).to.revertedWith( - "Owner did not sign this meta-transaction." + "Owner of the proxy account did not authorise the tx" ); } ); @@ -656,7 +756,7 @@ describe("ProxyAccountDeployer", () => { }); await expect(tx).to.be.revertedWith( - "Owner did not sign this meta-transaction." + "wner of the proxy account did not authorise the tx" ); } ); @@ -903,7 +1003,7 @@ describe("ProxyAccountDeployer", () => { }); await expect(tx).to.be.revertedWith( - "Owner did not sign this meta-transaction." + "wner of the proxy account did not authorise the tx" ); } ); @@ -1279,7 +1379,7 @@ describe("ProxyAccountDeployer", () => { data: deployProxy.data, }); - const echoData = echoCon.interface.functions.sendMessage.encode([ + const echoData = echoCon.interface.functions.onlyBroadcast.encode([ "hello", ]); const callData = msgSenderCon.interface.functions.test.encode([]); @@ -1290,14 +1390,14 @@ describe("ProxyAccountDeployer", () => { value: 0, data: callData, revertOnFail: true, - callType: CallType.DELEGATE, + callType: CallType.CALL, }, { to: echoCon.address, value: 0, data: echoData, revertOnFail: true, - callType: CallType.CALL, + callType: CallType.DELEGATE, }, ]; @@ -1338,11 +1438,8 @@ describe("ProxyAccountDeployer", () => { }); await expect(tx) - .to.emit(msgSenderCon, msgSenderCon.interface.events.WhoIsSender.name) - .withArgs(admin.address); - - const lastMessage = await echoCon.lastMessage(); - expect(lastMessage).to.eq("hello"); + .to.emit(echoCon, echoCon.interface.events.Broadcast.name) + .withArgs("hello"); } ).timeout(500000); diff --git a/test/contracts/relayHub.test.ts b/test/contracts/relayHub.test.ts index fc2bde8..2007791 100644 --- a/test/contracts/relayHub.test.ts +++ b/test/contracts/relayHub.test.ts @@ -334,14 +334,13 @@ describe("RelayHub Contract", () => { forwarderFactory, } = await loadFixture(createRelayHub); const msgSenderCall = msgSenderCon.interface.functions.test.encode([]); - const value = new BigNumber("0"); const encodedReplayProtection = defaultAbiCoder.encode( ["uint", "uint"], [0, 123] ); const encodedCallData = defaultAbiCoder.encode( - ["address", "uint", "bytes"], - [msgSenderCon.address, value, msgSenderCall] + ["uint", "address", "bytes"], + [CallType.CALL, msgSenderCon.address, msgSenderCall] ); // We expect encoded call data to include target contract address, the value, and the callData. @@ -351,21 +350,22 @@ describe("RelayHub Contract", () => { ReplayProtectionType.MULTINONCE, owner ); + // @ts-ignore: - const encodedData = forwarder.encodeMetaTransactionToSign( + const encodedTxData = forwarder.encodeMetaTransactionToSign( encodedCallData, encodedReplayProtection, - "0x0000000000000000000000000000000000000000" + AddressZero ); const signature = await owner.signMessage( - arrayify(keccak256(encodedData)) + arrayify(keccak256(encodedTxData)) ); const tx = relayHub .connect(sender) .forward( - { to: msgSenderCon.address, data: encodedCallData }, + { to: msgSenderCon.address, data: msgSenderCall }, encodedReplayProtection, "0x0000000000000000000000000000000000000000", owner.address, diff --git a/test/contracts/replayprotection.test.ts b/test/contracts/replayprotection.test.ts index 52d70a1..98fe002 100644 --- a/test/contracts/replayprotection.test.ts +++ b/test/contracts/replayprotection.test.ts @@ -7,8 +7,6 @@ import { ReplayProtectionWrapperFactory, ReplayProtectionWrapper, ChainID, - RelayHubFactory, - ReplayProtection, } from "../../src"; import { Provider } from "ethers/providers"; import { Wallet } from "ethers/wallet"; @@ -221,14 +219,14 @@ describe("ReplayProtection", () => { ).timeout("50000"); fnIt( - (a) => a.verifyPublic, + (a) => a.replayProtectionPublic, "access nonce() via verify (e.g. the replay protection authority)", async () => { const { replayProtection, admin } = await loadFixture( createReplayProtection ); - const { callData, encodedReplayProtection, signedCall } = await signCall( + const { encodedReplayProtection } = await signCall( replayProtection, AddressZero, admin, @@ -243,12 +241,10 @@ describe("ReplayProtection", () => { ) ); - await replayProtection.verifyPublic( - callData, - encodedReplayProtection, - AddressZero, + await replayProtection.replayProtectionPublic( admin.address, - signedCall + encodedReplayProtection, + AddressZero ); const nonce = await replayProtection.nonceStore(index); @@ -258,14 +254,14 @@ describe("ReplayProtection", () => { ); fnIt( - (a) => a.verifyPublic, + (a) => a.replayProtectionPublic, "for nonce() the same replay protection is used twice and should fail", async () => { const { replayProtection, admin } = await loadFixture( createReplayProtection ); - const { callData, encodedReplayProtection, signedCall } = await signCall( + const { encodedReplayProtection } = await signCall( replayProtection, AddressZero, admin, @@ -280,24 +276,20 @@ describe("ReplayProtection", () => { ) ); - await replayProtection.verifyPublic( - callData, - encodedReplayProtection, - AddressZero, + await replayProtection.replayProtectionPublic( admin.address, - signedCall + encodedReplayProtection, + AddressZero ); const nonce = await replayProtection.nonceStore(index); expect(nonce).to.eq(1); - const tx = replayProtection.verifyPublic( - callData, - encodedReplayProtection, - AddressZero, + const tx = replayProtection.replayProtectionPublic( admin.address, - signedCall + encodedReplayProtection, + AddressZero ); await expect(tx).to.be.revertedWith( @@ -307,14 +299,14 @@ describe("ReplayProtection", () => { ); fnIt( - (a) => a.verifyPublic, + (a) => a.replayProtectionPublic, "for nonce(), the nonce is used (nonce==1) is not yet valid (out of order) and should fail", async () => { const { replayProtection, admin } = await loadFixture( createReplayProtection ); - const { callData, encodedReplayProtection, signedCall } = await signCall( + const { encodedReplayProtection } = await signCall( replayProtection, AddressZero, admin, @@ -322,12 +314,10 @@ describe("ReplayProtection", () => { 1 ); - const tx = replayProtection.verifyPublic( - callData, - encodedReplayProtection, - AddressZero, + const tx = replayProtection.replayProtectionPublic( admin.address, - signedCall + encodedReplayProtection, + AddressZero ); await expect(tx).to.be.revertedWith( @@ -336,35 +326,6 @@ describe("ReplayProtection", () => { } ); - fnIt( - (a) => a.verifyPublic, - "access nonce() via verify fails as the nonce queue (nonce1) is the wrong replay protection authority.", - async () => { - const { replayProtection, admin } = await loadFixture( - createReplayProtection - ); - - const { callData, encodedReplayProtection, signedCall } = await signCall( - replayProtection, - "0x0000000000000000000000000000000000000000", - admin, - 0, - 1 - ); - - const tx = replayProtection.verifyPublic( - callData, - encodedReplayProtection, - "0x0000000000000000000000000000000000000001", - admin.address, - signedCall - ); - - // User signs AddressZero, but the contract computes signature for AddressOne - await expect(tx).to.be.revertedWith("Not expected signer"); - } - ); - fnIt( (a) => a.bitflipPublic, "flip a single bit to test BITFLIP", @@ -524,7 +485,7 @@ describe("ReplayProtection", () => { ); fnIt( - (a) => a.verifyPublic, + (a) => a.replayProtectionPublic, "access bitflip() via verify (e.g. the replay protection authority)", async () => { const { replayProtection, admin } = await loadFixture( @@ -534,7 +495,7 @@ describe("ReplayProtection", () => { const bitmapIndex = 123; const bitToFlip = flipBit(new BigNumber("0"), new BigNumber("0")); - const { callData, encodedReplayProtection, signedCall } = await signCall( + const { encodedReplayProtection } = await signCall( replayProtection, "0x0000000000000000000000000000000000000001", admin, @@ -553,12 +514,10 @@ describe("ReplayProtection", () => { ) ); - await replayProtection.verifyPublic( - callData, - encodedReplayProtection, - "0x0000000000000000000000000000000000000001", + await replayProtection.replayProtectionPublic( admin.address, - signedCall + encodedReplayProtection, + "0x0000000000000000000000000000000000000001" ); const nonce = await replayProtection.nonceStore(index); @@ -568,37 +527,7 @@ describe("ReplayProtection", () => { ); fnIt( - (a) => a.verifyPublic, - "access bitflip() via verify with wrong replay protection authority and it should fail", - async () => { - const { replayProtection, admin } = await loadFixture( - createReplayProtection - ); - - const bitmapIndex = 6175; - const bitToFlip = flipBit(new BigNumber("0"), new BigNumber("0")); - - const { callData, encodedReplayProtection, signedCall } = await signCall( - replayProtection, - "0x0000000000000000000000000000000000000000", - admin, - bitmapIndex, - bitToFlip.toNumber() - ); - - await expect( - replayProtection.verifyPublic( - callData, - encodedReplayProtection, - "0x0000000000000000000000000000000000000000", - admin.address, - signedCall - ) - ).to.be.revertedWith("Multinonce replay protection failed"); - } - ); - fnIt( - (a) => a.verifyPublic, + (a) => a.replayProtectionPublic, "submit same replay protection for bitflip() twice and it should fail", async () => { const { replayProtection, admin } = await loadFixture( @@ -608,7 +537,7 @@ describe("ReplayProtection", () => { const bitmapIndex = 321; const bitToFlip = flipBit(new BigNumber("0"), new BigNumber("0")); - const { callData, encodedReplayProtection, signedCall } = await signCall( + const { encodedReplayProtection } = await signCall( replayProtection, "0x0000000000000000000000000000000000000001", admin, @@ -616,12 +545,10 @@ describe("ReplayProtection", () => { bitToFlip.toNumber() ); - await replayProtection.verifyPublic( - callData, - encodedReplayProtection, - "0x0000000000000000000000000000000000000001", + await replayProtection.replayProtectionPublic( admin.address, - signedCall + encodedReplayProtection, + "0x0000000000000000000000000000000000000001" ); const index = keccak256( @@ -639,12 +566,10 @@ describe("ReplayProtection", () => { expect(nonce).to.eq(bitToFlip); await expect( - replayProtection.verifyPublic( - callData, - encodedReplayProtection, - "0x0000000000000000000000000000000000000001", + replayProtection.replayProtectionPublic( admin.address, - signedCall + encodedReplayProtection, + "0x0000000000000000000000000000000000000001" ) ).to.be.revertedWith("Bitflip replay protection failed"); } @@ -656,7 +581,7 @@ describe("ReplayProtection", () => { } fnIt( - (a) => a.verifyPublic, + (a) => a.replayProtectionPublic, "test isPowerOfTwo", async () => { // As long as one argument is zero, it should always work. @@ -675,7 +600,7 @@ describe("ReplayProtection", () => { ); fnIt( - (a) => a.verifyPublic, + (a) => a.replayProtectionPublic, "flips two bits for bitflip for a single job and it should fail", async () => { const { replayProtection, admin } = await loadFixture( @@ -686,7 +611,7 @@ describe("ReplayProtection", () => { let bitToFlip = flipBit(new BigNumber("0"), new BigNumber("0")); bitToFlip = flipBit(bitToFlip, new BigNumber("1")); - const { callData, encodedReplayProtection, signedCall } = await signCall( + const { encodedReplayProtection } = await signCall( replayProtection, "0x0000000000000000000000000000000000000001", admin, @@ -695,12 +620,10 @@ describe("ReplayProtection", () => { ); await expect( - replayProtection.verifyPublic( - callData, - encodedReplayProtection, - "0x0000000000000000000000000000000000000001", + replayProtection.replayProtectionPublic( admin.address, - signedCall + encodedReplayProtection, + "0x0000000000000000000000000000000000000001" ) ).to.be.revertedWith("Only a single bit can be flipped"); @@ -721,7 +644,7 @@ describe("ReplayProtection", () => { ); fnIt( - (a) => a.verifyPublic, + (a) => a.replayProtectionPublic, "flip a bit and add 1. use the modified bit to flip and it should fail.", async () => { const { replayProtection, admin } = await loadFixture( @@ -732,7 +655,7 @@ describe("ReplayProtection", () => { let bitToFlip = flipBit(new BigNumber("0"), new BigNumber("5")); bitToFlip = bitToFlip.add(1); - const { callData, encodedReplayProtection, signedCall } = await signCall( + const { encodedReplayProtection } = await signCall( replayProtection, "0x0000000000000000000000000000000000000001", admin, @@ -741,12 +664,10 @@ describe("ReplayProtection", () => { ); await expect( - replayProtection.verifyPublic( - callData, - encodedReplayProtection, - "0x0000000000000000000000000000000000000001", + replayProtection.replayProtectionPublic( admin.address, - signedCall + encodedReplayProtection, + "0x0000000000000000000000000000000000000001" ) ).to.be.revertedWith("Only a single bit can be flipped"); @@ -767,7 +688,7 @@ describe("ReplayProtection", () => { ); fnIt( - (a) => a.verifyPublic, + (a) => a.replayProtectionPublic, "submit same replay protection for bitflip() twice and it should fail", async () => { const { replayProtection, admin } = await loadFixture( @@ -777,7 +698,7 @@ describe("ReplayProtection", () => { const bitmapIndex = 321; const bitToFlip = flipBit(new BigNumber("0"), new BigNumber("0")); - const { callData, encodedReplayProtection, signedCall } = await signCall( + const { encodedReplayProtection } = await signCall( replayProtection, "0x0000000000000000000000000000000000000001", admin, @@ -785,12 +706,10 @@ describe("ReplayProtection", () => { bitToFlip.toNumber() ); - await replayProtection.verifyPublic( - callData, - encodedReplayProtection, - "0x0000000000000000000000000000000000000001", + await replayProtection.replayProtectionPublic( admin.address, - signedCall + encodedReplayProtection, + "0x0000000000000000000000000000000000000001" ); const index = keccak256( @@ -808,53 +727,12 @@ describe("ReplayProtection", () => { expect(nonce).to.eq(bitToFlip); await expect( - replayProtection.verifyPublic( - callData, - encodedReplayProtection, - "0x0000000000000000000000000000000000000001", + replayProtection.replayProtectionPublic( admin.address, - signedCall + encodedReplayProtection, + "0x0000000000000000000000000000000000000001" ) ).to.be.revertedWith("Bitflip replay protection failed"); } ); - - fnIt( - (a) => a.verifyPublic, - "catch the ReplayProtectionInfo event", - async () => { - const { replayProtection, admin } = await loadFixture( - createReplayProtection - ); - - const { - callData, - encodedReplayProtection, - signedCall, - txid, - } = await signCall(replayProtection, AddressZero, admin, 0, 0); - - const index = keccak256( - defaultAbiCoder.encode( - ["address", "uint", "address"], - [admin.address, 0, AddressZero] - ) - ); - - const tx = replayProtection.verifyPublic( - callData, - encodedReplayProtection, - AddressZero, - admin.address, - signedCall - ); - - await expect(tx) - .to.emit( - replayProtection, - replayProtection.interface.events.ReplayProtectionInfo.name - ) - .withArgs(AddressZero, encodedReplayProtection, txid); - } - ); }); diff --git a/test/contracts/singleSigner.test.ts b/test/contracts/singleSigner.test.ts new file mode 100644 index 0000000..ee412e4 --- /dev/null +++ b/test/contracts/singleSigner.test.ts @@ -0,0 +1,124 @@ +import "mocha"; +import * as chai from "chai"; +import { solidity, loadFixture } from "ethereum-waffle"; + +import { fnIt } from "@pisa-research/test-utils"; +import { ChainID, SingleSignerFactory, SingleSigner } from "../../src"; +import { Provider } from "ethers/providers"; +import { Wallet } from "ethers/wallet"; +import { defaultAbiCoder, keccak256, arrayify, BigNumber } from "ethers/utils"; +import { AddressZero } from "ethers/constants"; +import { Contract } from "ethers"; + +const expect = chai.expect; +chai.use(solidity); + +let dummyAccount: SingleSigner; +type singleSignerFunctions = typeof dummyAccount.functions; + +async function signCall( + replayProtection: Contract, + replayProtectionAuthority: string, + signer: Wallet, + nonce1: number, + nonce2: number +) { + const callData = "0x00000123123123123"; + const encodedReplayProtection = defaultAbiCoder.encode( + ["uint", "uint"], + [nonce1, nonce2] + ); + + const encodedMessage = defaultAbiCoder.encode( + ["bytes", "bytes", "address", "address", "uint"], + [ + callData, + encodedReplayProtection, + replayProtectionAuthority, + replayProtection.address, + ChainID.MAINNET, + ] + ); + + const txid = keccak256(encodedMessage); + const signedCall = await signer.signMessage(arrayify(txid)); + + return { + callData, + encodedReplayProtection, + signedCall, + txid, + }; +} + +async function createSingleSigner( + provider: Provider, + [admin, owner]: Wallet[] +) { + const singleSigner = await new SingleSignerFactory(admin).deploy(); + + return { + provider, + singleSigner, + admin, + owner, + }; +} + +describe("SingleSigner", () => { + fnIt( + (a) => a.authenticate, + "authenticate does not revert and thus the signature verification passes", + async () => { + const { singleSigner, admin } = await loadFixture(createSingleSigner); + + await singleSigner.init(admin.address); + + expect(await singleSigner.owner()).to.eq(admin.address); + + const messageHash = keccak256( + defaultAbiCoder.encode(["string"], ["hello"]) + ); + const signedMessage = await admin.signMessage(arrayify(messageHash)); + + await expect(singleSigner.authenticate(messageHash, signedMessage)).not.to + .be.reverted; + } + ); + + fnIt( + (a) => a.authenticate, + "the signer did not sign this message and thus authenticate reverts.", + async () => { + const { singleSigner, admin } = await loadFixture(createSingleSigner); + + await singleSigner.init(admin.address); + const messageHash = keccak256( + defaultAbiCoder.encode(["string"], ["hello"]) + ); + const signedMessage = await admin.signMessage(arrayify(messageHash)); + + await expect( + singleSigner.authenticate(keccak256(messageHash), signedMessage) + ).to.be.revertedWith( + "Owner of the proxy account did not authorise the tx" + ); + } + ); + + fnIt( + (a) => a.authenticate, + "forgot to init() the single signer and the signature verification fails", + async () => { + const { singleSigner, admin } = await loadFixture(createSingleSigner); + + const messageHash = keccak256( + defaultAbiCoder.encode(["string"], ["hello"]) + ); + const signedMessage = await admin.signMessage(arrayify(messageHash)); + + await expect(singleSigner.authenticate(messageHash, signedMessage)).to.be + .reverted; + } + ); +});