From d72976fd0834f034df1c3c6b068c95e5f6110d59 Mon Sep 17 00:00:00 2001 From: franciscotobar <100875069+franciscotobar@users.noreply.github.com> Date: Tue, 6 Aug 2024 08:45:37 -0600 Subject: [PATCH] Boltz verifier claim amount (#165) * feat: boltz verifier claim amount * docs: add the description to specify what it does * build: version --------- Co-authored-by: Antonio --- .../smartwallet/MinimalBoltzSmartWallet.sol | 4 +- contracts/utils/BoltzBytesUtil.sol | 33 -- contracts/utils/BoltzUtils.sol | 89 ++++++ contracts/verifier/BoltzDeployVerifier.sol | 45 +-- contracts/verifier/BoltzRelayVerifier.sol | 47 +-- .../verifier/MinimalBoltzDeployVerifier.sol | 47 +-- package-lock.json | 4 +- package.json | 2 +- test/verifier/boltzDeployVerifier.test.ts | 286 ++++++++++++++++-- test/verifier/boltzRelayVerifier.test.ts | 269 +++++++++++----- .../minimalBoltzDeployVerifier.test.ts | 182 ----------- 11 files changed, 609 insertions(+), 399 deletions(-) delete mode 100644 contracts/utils/BoltzBytesUtil.sol create mode 100644 contracts/utils/BoltzUtils.sol diff --git a/contracts/smartwallet/MinimalBoltzSmartWallet.sol b/contracts/smartwallet/MinimalBoltzSmartWallet.sol index 1df74361..dbcca60a 100644 --- a/contracts/smartwallet/MinimalBoltzSmartWallet.sol +++ b/contracts/smartwallet/MinimalBoltzSmartWallet.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier:MIT pragma solidity ^0.6.12; -import "../utils/BoltzBytesUtil.sol"; +import "../utils/BoltzUtils.sol"; /* solhint-disable no-inline-assembly */ /* solhint-disable avoid-low-level-calls */ @@ -32,7 +32,7 @@ contract MinimalBoltzSmartWallet { _isInitialized = true; - BoltzBytesUtil.validateClaimSignature(data); + BoltzUtils.validateClaimSignature(data); (bool success, bytes memory ret) = to.call(data); if (!success) { diff --git a/contracts/utils/BoltzBytesUtil.sol b/contracts/utils/BoltzBytesUtil.sol deleted file mode 100644 index 15730d5a..00000000 --- a/contracts/utils/BoltzBytesUtil.sol +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier:MIT -pragma solidity ^0.6.12; - -library BoltzBytesUtil { - //c3c37fbc => claim(bytes32,uint256,address,uint256) - bytes4 internal constant _EXTERNAL_SIGNATURE = 0xc3c37fbc; - - //cd413efa => claim(bytes32,uint256,address,address,uint256) - bytes4 internal constant _PUBLIC_SIGNATURE = 0xcd413efa; - - function validateClaimSignature(bytes memory data) internal pure { - bytes4 signature = toBytes4(data, 0); - - if ( - signature != _EXTERNAL_SIGNATURE && signature != _PUBLIC_SIGNATURE - ) { - revert("Method not allowed"); - } - } - - function toBytes4( - bytes memory input, - uint256 offset - ) internal pure returns (bytes4) { - bytes4 output; - - assembly { - output := mload(add(add(input, 0x20), offset)) - } - - return output; - } -} diff --git a/contracts/utils/BoltzUtils.sol b/contracts/utils/BoltzUtils.sol new file mode 100644 index 00000000..187f1ef4 --- /dev/null +++ b/contracts/utils/BoltzUtils.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +import "../interfaces/NativeSwap.sol"; + +library BoltzUtils { + //c3c37fbc => claim(bytes32,uint256,address,uint256) + bytes4 internal constant _EXTERNAL_SIGNATURE = 0xc3c37fbc; + + //cd413efa => claim(bytes32,uint256,address,address,uint256) + bytes4 internal constant _PUBLIC_SIGNATURE = 0xcd413efa; + + function validateClaimSignature(bytes memory data) internal pure { + bytes4 signature = toBytes4(data, 0); + + if ( + signature != _EXTERNAL_SIGNATURE && signature != _PUBLIC_SIGNATURE + ) { + revert("Method not allowed"); + } + } + + function toBytes4( + bytes memory input, + uint256 offset + ) internal pure returns (bytes4) { + bytes4 output; + + /* solhint-disable-next-line no-inline-assembly */ + assembly { + output := mload(add(add(input, 0x20), offset)) + } + + return output; + } + + /** + * @dev Decode the claim data and validate if + * a swap is stored in the Swap contract. + * @param data The claim data. + * @param to The address of the Swap contract. + * @param contractAddr The claiming address. + * @return The amount of RBTC to be claimed. + */ + function validateClaim( + bytes calldata data, + address to, + address contractAddr + ) internal returns (uint256) { + bytes4 signature = toBytes4(data, 0); + /* solhint-disable-next-line no-unused-vars */ + NativeSwap.PublicClaimInfo memory claim; + + if (signature == _EXTERNAL_SIGNATURE) { + NativeSwap.ExternalClaimInfo memory localClaim = abi.decode( + data[4:], + (NativeSwap.ExternalClaimInfo) + ); + claim = NativeSwap.PublicClaimInfo( + localClaim.preimage, + localClaim.amount, + contractAddr, + localClaim.refundAddress, + localClaim.timelock + ); + } else if (signature == _PUBLIC_SIGNATURE) { + claim = abi.decode(data[4:], (NativeSwap.PublicClaimInfo)); + } else { + revert("Method not allowed"); + } + + NativeSwap swap = NativeSwap(to); + + bytes32 preimageHash = sha256(abi.encodePacked(claim.preimage)); + + bytes32 hashValue = swap.hashValues( + preimageHash, + claim.amount, + claim.claimAddress, + claim.refundAddress, + claim.timelock + ); + + require(swap.swaps(hashValue), "Verifier: swap has no RBTC"); + + return claim.amount; + } +} diff --git a/contracts/verifier/BoltzDeployVerifier.sol b/contracts/verifier/BoltzDeployVerifier.sol index f334b75c..3fc6a7d6 100644 --- a/contracts/verifier/BoltzDeployVerifier.sol +++ b/contracts/verifier/BoltzDeployVerifier.sol @@ -11,6 +11,7 @@ import "../factory/BoltzSmartWalletFactory.sol"; import "../interfaces/IDeployVerifier.sol"; import "../interfaces/EnvelopingTypes.sol"; import "../utils/ContractValidator.sol"; +import "../utils/BoltzUtils.sol"; /** * A Verifier to be used on deploys. @@ -48,25 +49,33 @@ contract BoltzDeployVerifier is destinationContractValidation(relayRequest.request.to); - if (relayRequest.request.tokenContract != address(0)) { - require( - tokens[relayRequest.request.tokenContract], - "Token contract not allowed" - ); + if (relayRequest.request.tokenAmount > 0) { + if (relayRequest.request.tokenContract != address(0)) { + require( + tokens[relayRequest.request.tokenContract], + "Token contract not allowed" + ); - require( - relayRequest.request.tokenAmount <= - IERC20(relayRequest.request.tokenContract).balanceOf( - contractAddr - ), - "Token balance too low" - ); - } else { - require( - relayRequest.request.tokenAmount <= - address(contractAddr).balance, - "Native balance too low" - ); + require( + relayRequest.request.tokenAmount <= + IERC20(relayRequest.request.tokenContract).balanceOf( + contractAddr + ), + "Token balance too low" + ); + } else { + uint256 amount = BoltzUtils.validateClaim( + relayRequest.request.data, + relayRequest.request.to, + contractAddr + ); + + require( + relayRequest.request.tokenAmount <= + address(contractAddr).balance + amount, + "Native balance too low" + ); + } } return ( diff --git a/contracts/verifier/BoltzRelayVerifier.sol b/contracts/verifier/BoltzRelayVerifier.sol index 7ab94fd7..ec049c67 100644 --- a/contracts/verifier/BoltzRelayVerifier.sol +++ b/contracts/verifier/BoltzRelayVerifier.sol @@ -11,6 +11,7 @@ import "../interfaces/IWalletFactory.sol"; import "../interfaces/IRelayVerifier.sol"; import "../interfaces/EnvelopingTypes.sol"; import "../utils/ContractValidator.sol"; +import "../utils/BoltzUtils.sol"; /* solhint-disable no-inline-assembly */ /* solhint-disable avoid-low-level-calls */ @@ -56,27 +57,35 @@ contract BoltzRelayVerifier is "SW different to template" ); - require( - contracts[relayRequest.request.to], - "Destination contract not allowed" - ); + destinationContractValidation(relayRequest.request.to); + + if (relayRequest.request.tokenAmount > 0) { + if (relayRequest.request.tokenContract != address(0)) { + require( + tokens[relayRequest.request.tokenContract], + "Token contract not allowed" + ); - if (relayRequest.request.tokenContract != address(0)) { - require( - tokens[relayRequest.request.tokenContract], - "Token contract not allowed" - ); + require( + relayRequest.request.tokenAmount <= + IERC20(relayRequest.request.tokenContract).balanceOf( + payer + ), + "Token balance too low" + ); + } else { + uint256 amount = BoltzUtils.validateClaim( + relayRequest.request.data, + relayRequest.request.to, + relayRequest.relayData.callForwarder + ); - require( - relayRequest.request.tokenAmount <= - IERC20(relayRequest.request.tokenContract).balanceOf(payer), - "Token balance too low" - ); - } else { - require( - relayRequest.request.tokenAmount <= address(payer).balance, - "Native balance too low" - ); + require( + relayRequest.request.tokenAmount <= + address(payer).balance + amount, + "Native balance too low" + ); + } } return ( diff --git a/contracts/verifier/MinimalBoltzDeployVerifier.sol b/contracts/verifier/MinimalBoltzDeployVerifier.sol index 222ac515..84ee4c25 100644 --- a/contracts/verifier/MinimalBoltzDeployVerifier.sol +++ b/contracts/verifier/MinimalBoltzDeployVerifier.sol @@ -10,7 +10,7 @@ import "../factory/MinimalBoltzSmartWalletFactory.sol"; import "../interfaces/IDeployVerifier.sol"; import "../interfaces/EnvelopingTypes.sol"; import "../utils/ContractValidator.sol"; -import "../utils/BoltzBytesUtil.sol"; +import "../utils/BoltzUtils.sol"; import "../interfaces/NativeSwap.sol"; /** @@ -53,7 +53,7 @@ contract MinimalBoltzDeployVerifier is destinationContractValidation(relayRequest.request.to); - uint256 amount = _validateClaim( + uint256 amount = BoltzUtils.validateClaim( relayRequest.request.data, relayRequest.request.to, contractAddr @@ -78,47 +78,4 @@ contract MinimalBoltzDeployVerifier is ) ); } - - function _validateClaim( - bytes calldata data, - address to, - address contractAddr - ) private returns (uint256) { - bytes4 signature = BoltzBytesUtil.toBytes4(data, 0); - NativeSwap.PublicClaimInfo memory claim; - - if (signature == BoltzBytesUtil._EXTERNAL_SIGNATURE) { - NativeSwap.ExternalClaimInfo memory localClaim = abi.decode( - data[4:], - (NativeSwap.ExternalClaimInfo) - ); - claim = NativeSwap.PublicClaimInfo( - localClaim.preimage, - localClaim.amount, - contractAddr, - localClaim.refundAddress, - localClaim.timelock - ); - } else if (signature == BoltzBytesUtil._PUBLIC_SIGNATURE) { - claim = abi.decode(data[4:], (NativeSwap.PublicClaimInfo)); - } else { - revert("Method not allowed"); - } - - NativeSwap swap = NativeSwap(to); - - bytes32 preimageHash = sha256(abi.encodePacked(claim.preimage)); - - bytes32 hashValue = swap.hashValues( - preimageHash, - claim.amount, - claim.claimAddress, - claim.refundAddress, - claim.timelock - ); - - require(swap.swaps(hashValue), "Verifier: swap has no RBTC"); - - return claim.amount; - } } diff --git a/package-lock.json b/package-lock.json index ddedc684..ff63d42e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@rsksmart/rif-relay-contracts", - "version": "2.1.0-beta.0", + "version": "2.1.1-beta.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@rsksmart/rif-relay-contracts", - "version": "2.1.0-beta.0", + "version": "2.1.1-beta.0", "license": "MIT", "dependencies": { "@metamask/eth-sig-util": "^4.0.1", diff --git a/package.json b/package.json index 29cef68c..900fca17 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rsksmart/rif-relay-contracts", - "version": "2.1.0-beta.0", + "version": "2.1.1-beta.0", "private": false, "description": "This project contains all the contracts needed for the rif relay system.", "license": "MIT", diff --git a/test/verifier/boltzDeployVerifier.test.ts b/test/verifier/boltzDeployVerifier.test.ts index 9444376f..20b6d67e 100644 --- a/test/verifier/boltzDeployVerifier.test.ts +++ b/test/verifier/boltzDeployVerifier.test.ts @@ -381,6 +381,70 @@ describe('BoltzDeployVerifier Contract', function () { }); }); + it('should not revert if paying with ERC20 token', async function () { + fakeToken.balanceOf.returns(BigNumber.from('200000000000')); + + const deployRequest: EnvelopingTypes.DeployRequestStruct = { + relayData: { + callForwarder: fakeWalletFactory.address, + callVerifier: deployVerifierMock.address, + gasPrice: '10', + feesReceiver: relayWorker.address, + }, + request: { + recoverer: constants.AddressZero, + index: '0', + data: '0x00', + from: owner.address, + to: fakeSwap.address, + nonce: '0', + tokenGas: '50000', + relayHub: fakeRelayHub.address, + tokenAmount: '100000000000', + tokenContract: fakeToken.address, + validUntilTime: '0', + value: '0', + }, + }; + + const result = deployVerifierMock.verifyRelayedCall( + deployRequest, + '0x00' + ); + await expect(result).to.not.be.reverted; + }); + + it('should not revert if not paying', async function () { + const deployRequest: EnvelopingTypes.DeployRequestStruct = { + relayData: { + callForwarder: fakeWalletFactory.address, + callVerifier: deployVerifierMock.address, + gasPrice: '10', + feesReceiver: relayWorker.address, + }, + request: { + recoverer: constants.AddressZero, + index: '0', + data: '0x00', + from: owner.address, + to: fakeSwap.address, + nonce: '0', + tokenGas: '0', + relayHub: fakeRelayHub.address, + tokenAmount: '0', + tokenContract: fakeToken.address, + validUntilTime: '0', + value: '0', + }, + }; + + const result = deployVerifierMock.verifyRelayedCall( + deployRequest, + '0x00' + ); + await expect(result).to.not.be.reverted; + }); + it('should revert if destination contract is not allowed', async function () { await deployVerifierMock.setVariables({ acceptedContracts: [], @@ -577,35 +641,199 @@ describe('BoltzDeployVerifier Contract', function () { await expect(result).to.be.revertedWith('Token balance too low'); }); - it('should revert if native token balance is too low', async function () { - const deployRequest: EnvelopingTypes.DeployRequestStruct = { - relayData: { - callForwarder: fakeWalletFactory.address, - callVerifier: deployVerifierMock.address, - gasPrice: '10', - feesReceiver: relayWorker.address, - }, - request: { - recoverer: constants.AddressZero, - index: '0', - data: '0x00', - from: owner.address, - to: fakeSwap.address, - nonce: '0', - tokenGas: '50000', - relayHub: fakeRelayHub.address, - tokenAmount: ethers.utils.parseEther('1'), - tokenContract: constants.AddressZero, - validUntilTime: '0', - value: '0', - }, - }; - - const result = deployVerifierMock.verifyRelayedCall( - deployRequest, - '0x00' - ); - await expect(result).to.be.revertedWith('Native balance too low'); + describe('calling claim', function () { + type ClaimMethodType = 'public' | 'external'; + + function testClaimMethodType(method: ClaimMethodType) { + describe(`${method} method`, function () { + let data: string; + let smartWalletAddress: string; + + beforeEach(function () { + fakeSwap.swaps.returns(true); + const ABI = [ + 'function claim(bytes32 preimage, uint amount, address claimAddress, address refundAddress, uint timelock)', + 'function claim(bytes32 preimage, uint amount, address refundAddress, uint timelock)', + ]; + const abiInterface = new ethers.utils.Interface(ABI); + smartWalletAddress = ethers.Wallet.createRandom().address; + switch (method) { + case 'external': + data = abiInterface.encodeFunctionData( + 'claim(bytes32,uint256,address,uint256)', + [ + constants.HashZero, + ethers.utils.parseEther('0.5'), + smartWalletAddress, + 500, + ] + ); + break; + case 'public': + data = abiInterface.encodeFunctionData( + 'claim(bytes32,uint256,address,address,uint256)', + [ + constants.HashZero, + ethers.utils.parseEther('0.5'), + smartWalletAddress, + constants.AddressZero, + 500, + ] + ); + break; + } + }); + + it('should revert if native balance is too low', async function () { + const deployRequest: EnvelopingTypes.DeployRequestStruct = { + relayData: { + callForwarder: fakeWalletFactory.address, + callVerifier: deployVerifierMock.address, + gasPrice: '10', + feesReceiver: relayWorker.address, + }, + request: { + recoverer: constants.AddressZero, + index: '0', + data, + from: owner.address, + to: fakeSwap.address, + nonce: '0', + tokenGas: '50000', + relayHub: fakeRelayHub.address, + tokenAmount: ethers.utils.parseEther('1'), + tokenContract: constants.AddressZero, + validUntilTime: '0', + value: '0', + }, + }; + + const result = deployVerifierMock.verifyRelayedCall( + deployRequest, + '0x00' + ); + await expect(result).to.be.revertedWith('Native balance too low'); + }); + + it('should revert if swap contract cannot pay for the native fee', async function () { + fakeWalletFactory.getSmartWalletAddress.returns(smartWalletAddress); + fakeSwap.swaps.returns(false); + + const deployRequest: EnvelopingTypes.DeployRequestStruct = { + relayData: { + callForwarder: fakeWalletFactory.address, + callVerifier: deployVerifierMock.address, + gasPrice: '10', + feesReceiver: relayWorker.address, + }, + request: { + recoverer: constants.AddressZero, + index: '0', + data, + from: owner.address, + to: fakeSwap.address, + nonce: '0', + tokenGas: '50000', + relayHub: fakeRelayHub.address, + tokenAmount: ethers.utils.parseEther('0.5'), + tokenContract: constants.AddressZero, + validUntilTime: '0', + value: '0', + }, + }; + + const result = deployVerifierMock.verifyRelayedCall( + deployRequest, + '0x00' + ); + await expect(result).to.be.revertedWith( + 'Verifier: swap has no RBTC' + ); + }); + + it('should revert if method is not allowed', async function () { + const ABI = [ + 'function claim(bytes32 preimage, uint amount, address claimAddress)', + ]; + const abiInterface = new ethers.utils.Interface(ABI); + const data = abiInterface.encodeFunctionData('claim', [ + constants.HashZero, + ethers.utils.parseEther('0.5'), + constants.AddressZero, + ]); + + const deployRequest: EnvelopingTypes.DeployRequestStruct = { + relayData: { + callForwarder: fakeWalletFactory.address, + callVerifier: deployVerifierMock.address, + gasPrice: '10', + feesReceiver: relayWorker.address, + }, + request: { + recoverer: constants.AddressZero, + index: '0', + data, + from: owner.address, + to: fakeSwap.address, + nonce: '0', + tokenGas: '50000', + relayHub: fakeRelayHub.address, + tokenAmount: '100000', + tokenContract: constants.AddressZero, + validUntilTime: '0', + value: '0', + }, + }; + + const result = deployVerifierMock.verifyRelayedCall( + deployRequest, + '0x00' + ); + await expect(result).to.be.revertedWith('Method not allowed'); + }); + + it('should not revert if swap contract can pay for the native fee', async function () { + fakeWalletFactory.getSmartWalletAddress.returns(smartWalletAddress); + + const deployRequest: EnvelopingTypes.DeployRequestStruct = { + relayData: { + callForwarder: fakeWalletFactory.address, + callVerifier: deployVerifierMock.address, + gasPrice: '10', + feesReceiver: relayWorker.address, + }, + request: { + recoverer: constants.AddressZero, + index: '0', + data, + from: owner.address, + to: fakeSwap.address, + nonce: '0', + tokenGas: '50000', + relayHub: fakeRelayHub.address, + tokenAmount: ethers.utils.parseEther('0.5'), + tokenContract: constants.AddressZero, + validUntilTime: '0', + value: '0', + }, + }; + + const result = deployVerifierMock.verifyRelayedCall( + deployRequest, + '0x00' + ); + await expect(result).to.not.be.reverted; + }); + }); + } + + // Using [dynamically generated tests](https://mochajs.org/#dynamically-generating-tests) + // we needs the mocha/no-setup-in-describe rule to be disabled + // see: https://github.com/lo1tuma/eslint-plugin-mocha/blob/main/docs/rules/no-setup-in-describe.md#disallow-setup-in-describe-blocks-mochano-setup-in-describe + // eslint-disable-next-line mocha/no-setup-in-describe + testClaimMethodType('public'); + // eslint-disable-next-line mocha/no-setup-in-describe + testClaimMethodType('external'); }); }); }); diff --git a/test/verifier/boltzRelayVerifier.test.ts b/test/verifier/boltzRelayVerifier.test.ts index c1055704..89b89ad8 100644 --- a/test/verifier/boltzRelayVerifier.test.ts +++ b/test/verifier/boltzRelayVerifier.test.ts @@ -14,6 +14,7 @@ import { BoltzRelayVerifier__factory, BoltzSmartWallet, BoltzSmartWalletFactory, + NativeSwap, } from 'typechain-types'; import { EnvelopingTypes, RelayHub } from 'typechain-types/contracts/RelayHub'; import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; @@ -414,47 +415,7 @@ describe('BoltzRelayVerifier Contract', function () { await expect(result).to.not.be.reverted; }); - it('should not revert if paying with native token', async function () { - fakeWalletFactory.runtimeCodeHash.returns( - '0xbc36789e7a1e281436464229828f817d6612f7b477d66591ff96a9e064bcc98a' - ); - - await relayWorker.sendTransaction({ - to: fakeSmartWallet.address, - value: ethers.utils.parseEther('1'), - }); - - const relayRequest: EnvelopingTypes.RelayRequestStruct = { - relayData: { - callForwarder: fakeSmartWallet.address, - callVerifier: relayVerifierMock.address, - gasPrice: '10', - feesReceiver: relayWorker.address, - }, - request: { - data: '0x00', - from: owner.address, - to: recipient.address, - gas: '1000000', - nonce: '0', - tokenGas: '50000', - relayHub: fakeRelayHub.address, - tokenAmount: '100000000000', - tokenContract: constants.AddressZero, - validUntilTime: '0', - value: '0', - }, - }; - - await relayVerifierMock.verifyRelayedCall(relayRequest, '0x00'); - // await expect(result).to.not.be.reverted; - }); - it('should not revert if not paying', async function () { - fakeWalletFactory.runtimeCodeHash.returns( - '0xbc36789e7a1e281436464229828f817d6612f7b477d66591ff96a9e064bcc98a' - ); - const relayRequest: EnvelopingTypes.RelayRequestStruct = { relayData: { callForwarder: fakeSmartWallet.address, @@ -468,7 +429,7 @@ describe('BoltzRelayVerifier Contract', function () { to: recipient.address, gas: '1000000', nonce: '0', - tokenGas: '50000', + tokenGas: '0', relayHub: fakeRelayHub.address, tokenAmount: '0', tokenContract: fakeToken.address, @@ -580,33 +541,6 @@ describe('BoltzRelayVerifier Contract', function () { await expect(result).to.be.revertedWith('Token balance too low'); }); - it('should revert if native token balance is too low', async function () { - const relayRequest: EnvelopingTypes.RelayRequestStruct = { - relayData: { - callForwarder: fakeSmartWallet.address, - callVerifier: relayVerifierMock.address, - gasPrice: '10', - feesReceiver: relayWorker.address, - }, - request: { - data: '0x00', - from: owner.address, - to: recipient.address, - gas: '1000000', - nonce: '0', - tokenGas: '50000', - relayHub: fakeRelayHub.address, - tokenAmount: '100000000000', - tokenContract: constants.AddressZero, - validUntilTime: '0', - value: '0', - }, - }; - - const result = relayVerifierMock.verifyRelayedCall(relayRequest, '0x00'); - await expect(result).to.be.revertedWith('Native balance too low'); - }); - it('should revert if smart wallet template is different than smart wallet factory', async function () { fakeWalletFactory.runtimeCodeHash.returns( '0xbc36789e7a1e281436464229828f817d6612f7b477d66591ff96a9e064bcc98b' @@ -637,5 +571,204 @@ describe('BoltzRelayVerifier Contract', function () { const result = relayVerifierMock.verifyRelayedCall(relayRequest, '0x00'); await expect(result).to.be.revertedWith('SW different to template'); }); + + describe('calling claim', function () { + type ClaimMethodType = 'public' | 'external'; + + function testClaimMethodType(method: ClaimMethodType) { + describe(`${method} method`, function () { + let data: string; + let smartWalletAddress: string; + let fakeSwap: FakeContract; + + beforeEach(async function () { + fakeSwap = await smock.fake('NativeSwap'); + fakeSwap.swaps.returns(true); + await relayVerifierMock.setVariables({ + acceptedContracts: [fakeSwap.address], + contracts: { + [fakeSwap.address]: true, + }, + }); + const ABI = [ + 'function claim(bytes32 preimage, uint amount, address claimAddress, address refundAddress, uint timelock)', + 'function claim(bytes32 preimage, uint amount, address refundAddress, uint timelock)', + ]; + const abiInterface = new ethers.utils.Interface(ABI); + smartWalletAddress = ethers.Wallet.createRandom().address; + switch (method) { + case 'external': + data = abiInterface.encodeFunctionData( + 'claim(bytes32,uint256,address,uint256)', + [ + constants.HashZero, + ethers.utils.parseEther('0.5'), + smartWalletAddress, + 500, + ] + ); + break; + case 'public': + data = abiInterface.encodeFunctionData( + 'claim(bytes32,uint256,address,address,uint256)', + [ + constants.HashZero, + ethers.utils.parseEther('0.5'), + smartWalletAddress, + constants.AddressZero, + 500, + ] + ); + break; + } + }); + + it('should revert if native balance is too low', async function () { + const relayRequest: EnvelopingTypes.RelayRequestStruct = { + relayData: { + callForwarder: fakeWalletFactory.address, + callVerifier: relayVerifierMock.address, + gasPrice: '10', + feesReceiver: relayWorker.address, + }, + request: { + data, + from: owner.address, + to: fakeSwap.address, + gas: '1000000', + nonce: '0', + tokenGas: '50000', + relayHub: fakeRelayHub.address, + tokenAmount: ethers.utils.parseEther('1'), + tokenContract: constants.AddressZero, + validUntilTime: '0', + value: '0', + }, + }; + + const result = relayVerifierMock.verifyRelayedCall( + relayRequest, + '0x00' + ); + await expect(result).to.be.revertedWith('Native balance too low'); + }); + + it('should revert if swap contract cannot pay for the native fee', async function () { + fakeWalletFactory.getSmartWalletAddress.returns(smartWalletAddress); + fakeSwap.swaps.returns(false); + + const relayRequest: EnvelopingTypes.RelayRequestStruct = { + relayData: { + callForwarder: fakeWalletFactory.address, + callVerifier: relayVerifierMock.address, + gasPrice: '10', + feesReceiver: relayWorker.address, + }, + request: { + data, + from: owner.address, + to: fakeSwap.address, + gas: '1000000', + nonce: '0', + tokenGas: '50000', + relayHub: fakeRelayHub.address, + tokenAmount: ethers.utils.parseEther('0.5'), + tokenContract: constants.AddressZero, + validUntilTime: '0', + value: '0', + }, + }; + + const result = relayVerifierMock.verifyRelayedCall( + relayRequest, + '0x00' + ); + await expect(result).to.be.revertedWith( + 'Verifier: swap has no RBTC' + ); + }); + + it('should revert if method is not allowed', async function () { + const ABI = [ + 'function claim(bytes32 preimage, uint amount, address claimAddress)', + ]; + const abiInterface = new ethers.utils.Interface(ABI); + const data = abiInterface.encodeFunctionData('claim', [ + constants.HashZero, + ethers.utils.parseEther('0.5'), + constants.AddressZero, + ]); + + const deployRequest: EnvelopingTypes.RelayRequestStruct = { + relayData: { + callForwarder: fakeWalletFactory.address, + callVerifier: relayVerifierMock.address, + gasPrice: '10', + feesReceiver: relayWorker.address, + }, + request: { + data, + from: owner.address, + to: fakeSwap.address, + gas: '1000000', + nonce: '0', + tokenGas: '50000', + relayHub: fakeRelayHub.address, + tokenAmount: '100000', + tokenContract: constants.AddressZero, + validUntilTime: '0', + value: '0', + }, + }; + + const result = relayVerifierMock.verifyRelayedCall( + deployRequest, + '0x00' + ); + await expect(result).to.be.revertedWith('Method not allowed'); + }); + + it('should not revert if swap contract can pay for the native fee', async function () { + fakeWalletFactory.getSmartWalletAddress.returns(smartWalletAddress); + + const relayRequest: EnvelopingTypes.RelayRequestStruct = { + relayData: { + callForwarder: fakeWalletFactory.address, + callVerifier: relayVerifierMock.address, + gasPrice: '10', + feesReceiver: relayWorker.address, + }, + request: { + data, + from: owner.address, + to: fakeSwap.address, + gas: '1000000', + nonce: '0', + tokenGas: '50000', + relayHub: fakeRelayHub.address, + tokenAmount: ethers.utils.parseEther('0.5'), + tokenContract: constants.AddressZero, + validUntilTime: '0', + value: '0', + }, + }; + + const result = relayVerifierMock.verifyRelayedCall( + relayRequest, + '0x00' + ); + await expect(result).to.not.be.reverted; + }); + }); + } + + // Using [dynamically generated tests](https://mochajs.org/#dynamically-generating-tests) + // we needs the mocha/no-setup-in-describe rule to be disabled + // see: https://github.com/lo1tuma/eslint-plugin-mocha/blob/main/docs/rules/no-setup-in-describe.md#disallow-setup-in-describe-blocks-mochano-setup-in-describe + // eslint-disable-next-line mocha/no-setup-in-describe + testClaimMethodType('public'); + // eslint-disable-next-line mocha/no-setup-in-describe + testClaimMethodType('external'); + }); }); }); diff --git a/test/verifier/minimalBoltzDeployVerifier.test.ts b/test/verifier/minimalBoltzDeployVerifier.test.ts index f3b5c4a3..e6d44aef 100644 --- a/test/verifier/minimalBoltzDeployVerifier.test.ts +++ b/test/verifier/minimalBoltzDeployVerifier.test.ts @@ -385,188 +385,6 @@ describe('MinimalBoltzDeployVerifier Contract', function () { await expect(result).to.be.revertedWith('Method not allowed'); }); - describe('public method', function () { - let data: string; - let smartWalletAddress: string; - - beforeEach(function () { - fakeSwap.swaps.returns(true); - const ABI = [ - 'function claim(bytes32 preimage, uint amount, address claimAddress, address refundAddress, uint timelock)', - ]; - const abiInterface = new ethers.utils.Interface(ABI); - smartWalletAddress = ethers.Wallet.createRandom().address; - data = abiInterface.encodeFunctionData('claim', [ - constants.HashZero, - ethers.utils.parseEther('0.5'), - smartWalletAddress, - constants.AddressZero, - 500, - ]); - }); - - it('should revert if claiming value is too low', async function () { - const deployRequest: EnvelopingTypes.DeployRequestStruct = { - relayData: { - callForwarder: fakeWalletFactory.address, - callVerifier: deployVerifierMock.address, - gasPrice: '10', - feesReceiver: relayWorker.address, - }, - request: { - recoverer: constants.AddressZero, - index: '0', - data, - from: owner.address, - to: fakeSwap.address, - nonce: '0', - tokenGas: '50000', - relayHub: fakeRelayHub.address, - tokenAmount: ethers.utils.parseEther('1'), - tokenContract: constants.AddressZero, - validUntilTime: '0', - value: '0', - }, - }; - - const result = deployVerifierMock.verifyRelayedCall( - deployRequest, - '0x00' - ); - await expect(result).to.be.revertedWith('Native balance too low'); - }); - - it('should revert if swap contract cannot pay for the native fee', async function () { - fakeWalletFactory.getSmartWalletAddress.returns(smartWalletAddress); - fakeSwap.swaps.returns(false); - - const deployRequest: EnvelopingTypes.DeployRequestStruct = { - relayData: { - callForwarder: fakeWalletFactory.address, - callVerifier: deployVerifierMock.address, - gasPrice: '10', - feesReceiver: relayWorker.address, - }, - request: { - recoverer: constants.AddressZero, - index: '0', - data, - from: owner.address, - to: fakeSwap.address, - nonce: '0', - tokenGas: '50000', - relayHub: fakeRelayHub.address, - tokenAmount: ethers.utils.parseEther('0.5'), - tokenContract: constants.AddressZero, - validUntilTime: '0', - value: '0', - }, - }; - - const result = deployVerifierMock.verifyRelayedCall( - deployRequest, - '0x00' - ); - await expect(result).to.be.revertedWith('Verifier: swap has no RBTC'); - }); - - it('should not revert if swap contract can pay for the native fee', async function () { - fakeWalletFactory.getSmartWalletAddress.returns(smartWalletAddress); - - const deployRequest: EnvelopingTypes.DeployRequestStruct = { - relayData: { - callForwarder: fakeWalletFactory.address, - callVerifier: deployVerifierMock.address, - gasPrice: '10', - feesReceiver: relayWorker.address, - }, - request: { - recoverer: constants.AddressZero, - index: '0', - data, - from: owner.address, - to: fakeSwap.address, - nonce: '0', - tokenGas: '50000', - relayHub: fakeRelayHub.address, - tokenAmount: ethers.utils.parseEther('0.5'), - tokenContract: constants.AddressZero, - validUntilTime: '0', - value: '0', - }, - }; - - const result = deployVerifierMock.verifyRelayedCall( - deployRequest, - '0x00' - ); - await expect(result).to.not.be.reverted; - }); - - it('should not revert if not paying', async function () { - const deployRequest: EnvelopingTypes.DeployRequestStruct = { - relayData: { - callForwarder: fakeWalletFactory.address, - callVerifier: deployVerifierMock.address, - gasPrice: '10', - feesReceiver: relayWorker.address, - }, - request: { - recoverer: constants.AddressZero, - index: '0', - data, - from: owner.address, - to: fakeSwap.address, - nonce: '0', - tokenGas: '0', - relayHub: fakeRelayHub.address, - tokenAmount: '0', - tokenContract: constants.AddressZero, - validUntilTime: '0', - value: '0', - }, - }; - - const result = deployVerifierMock.verifyRelayedCall( - deployRequest, - '0x00' - ); - - await expect(result).to.not.be.reverted; - }); - - it('should revert if paying with ERC20 token', async function () { - const deployRequest: EnvelopingTypes.DeployRequestStruct = { - relayData: { - callForwarder: fakeWalletFactory.address, - callVerifier: deployVerifierMock.address, - gasPrice: '10', - feesReceiver: relayWorker.address, - }, - request: { - recoverer: constants.AddressZero, - index: '0', - data, - from: owner.address, - to: fakeSwap.address, - nonce: '0', - tokenGas: '50000', - relayHub: fakeRelayHub.address, - tokenAmount: '100000000000', - tokenContract: fakeToken.address, - validUntilTime: '0', - value: '0', - }, - }; - - const result = deployVerifierMock.verifyRelayedCall( - deployRequest, - '0x00' - ); - await expect(result).to.be.rejectedWith('RBTC necessary for payment'); - }); - }); - describe('calling claim', function () { type ClaimMethodType = 'public' | 'external';