diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4ac844df..89f522b9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,3 +31,31 @@ jobs: run: | forge test --fork-url ${{ secrets.OPTIMISM_GOERLI_RPC_URL }} --etherscan-api-key ${{ secrets.ETHERSCAN_API_KEY }} -vvv id: test + + hardhat_test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Check out repository code + uses: actions/checkout@v2 + + - name: Building on Node.js + uses: actions/setup-node@v2 + with: + node-version: '18.12.0' + + - name: Update NPM + run: npm install -g npm@8.19.2 + - uses: actions/cache@v2 + with: + path: '**/node_modules' + key: ${{ runner.os }}-modules-${{ hashFiles('**/package-lock.json') }} + + - name: Install dependencies + run: npm i --no-audit + + - name: Execute contract tests + run: npx hardhat test diff --git a/.gitignore b/.gitignore index 9270291a..c1e64e8e 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,21 @@ out/ # Other .vscode .idea + +# Hardhat +node_modules +.env +package-lock.json + +# Hardhat files +/cache_hardhat +/cache +/artifacts + +# TypeChain files +/typechain +/typechain-types + +# solidity-coverage files +/coverage +/coverage.json diff --git a/hardhat.config.ts b/hardhat.config.ts new file mode 100644 index 00000000..2ec68bd9 --- /dev/null +++ b/hardhat.config.ts @@ -0,0 +1,37 @@ +import { HardhatUserConfig } from "hardhat/config"; +import "@nomicfoundation/hardhat-toolbox"; +import "hardhat-preprocessor"; +import fs from "fs"; + +function getRemappings() { + return fs + .readFileSync("remappings.txt", "utf8") + .split("\n") + .filter(Boolean) // remove empty lines + .map((line) => line.trim().split("=")); +} + +const config: HardhatUserConfig = { + solidity: "0.8.20", + preprocess: { + eachLine: (hre) => ({ + transform: (line: string) => { + if (line.match(/^\s*import /i)) { + for (const [from, to] of getRemappings()) { + if (line.includes(from)) { + line = line.replace(from, to); + break; + } + } + } + return line; + }, + }), + }, + paths: { + sources: "./src", + cache: "./cache_hardhat", + }, +}; + +export default config; diff --git a/package.json b/package.json index 9f2018c5..4da9eae7 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,9 @@ "bugs": { "url": "https://github.com/Kwenta/smart-margin-v3/issues" }, - "devDependencies": {}, - "dependencies": {} -} \ No newline at end of file + "devDependencies": { + "@nomicfoundation/hardhat-toolbox": "^4.0.0", + "hardhat": "^2.19.1", + "hardhat-preprocessor": "^0.1.5" + } +} diff --git a/remappings.txt b/remappings.txt new file mode 100644 index 00000000..1cf4579e --- /dev/null +++ b/remappings.txt @@ -0,0 +1,7 @@ +@openzeppelin/contracts/=lib/trusted-multicall-forwarder/lib/openzeppelin-contracts/contracts/ +ds-test/=lib/forge-std/lib/ds-test/src/ +erc4626-tests/=lib/trusted-multicall-forwarder/lib/openzeppelin-contracts/lib/erc4626-tests/ +forge-std/=lib/forge-std/src/ +openzeppelin-contracts/=lib/trusted-multicall-forwarder/lib/openzeppelin-contracts/ +synthetix-v3/=lib/synthetix-v3/ +trusted-multicall-forwarder/=lib/trusted-multicall-forwarder/src/ diff --git a/src/libraries/ConditionalOrderHashLib.sol b/src/libraries/ConditionalOrderHashLib.sol index 67d4c771..95a6d36c 100644 --- a/src/libraries/ConditionalOrderHashLib.sol +++ b/src/libraries/ConditionalOrderHashLib.sol @@ -52,7 +52,7 @@ library ConditionalOrderHashLib { // array of dynamic length bytes must be hashed separately // to create an array of fixed length bytes32 hashes - bytes32[] memory hashedConditions; + bytes32[] memory hashedConditions = new bytes32[](co.conditions.length); for (uint256 i = 0; i < co.conditions.length; i++) { hashedConditions[i] = keccak256(co.conditions[i]); } @@ -66,7 +66,7 @@ library ConditionalOrderHashLib { co.requireVerified, co.trustedExecutor, co.maxExecutorFee, - keccak256(abi.encode(hashedConditions)) + keccak256(abi.encodePacked(hashedConditions)) ) ); } diff --git a/test/Signature.test.ts b/test/Signature.test.ts new file mode 100644 index 00000000..c444a5a9 --- /dev/null +++ b/test/Signature.test.ts @@ -0,0 +1,138 @@ +import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers"; +import { expect } from "chai"; +import { ethers } from "hardhat"; + +const ONE_ADDRESS = "0x0000000000000000000000000000000000000001"; + +// example data +const signerPrivateKey = + "0xe690d00bd51f5343c6999d8e88328e3dfa0111b65f2a8790d48f89fe43ad07c0"; +const marketId = "200"; +const accountId = "170141183460469231731687303715884105756"; +const sizeDelta = "1000000000000000000"; +const settlementStrategyId = "0"; +const acceptablePrice = + "115792089237316195423570985008687907853269984665640564039457584007913129639935"; +const isReduceOnly = false; +const trackingCode = + "0x4b57454e54410000000000000000000000000000000000000000000000000000"; +const referrer = "0xF510a2Ff7e9DD7e18629137adA4eb56B9c13E885"; +const nonce = "0"; +const requireVerified = false; +const trustedExecutor = "0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496"; +const maxExecutorFee = + "115792089237316195423570985008687907853269984665640564039457584007913129639935"; +// 0x6162630000000000000000000000000000000000000000000000000000000000 = abc +// 0x6465660000000000000000000000000000000000000000000000000000000000 = def +// 0x6768690000000000000000000000000000000000000000000000000000000000 = ghi +const conditions: string[] = [ + "0x6162630000000000000000000000000000000000000000000000000000000000", + "0x6465660000000000000000000000000000000000000000000000000000000000", + "0x6768690000000000000000000000000000000000000000000000000000000000", +]; + +describe("Signature", function () { + // We define a fixture to reuse the same setup in every test. + // We use loadFixture to run this setup once, snapshot that state, + // and reset Hardhat Network to that snapshot in every test. + async function bootstrapSystem() { + // Contracts are deployed using the first signer/account by default + const [owner, otherAccount] = await ethers.getSigners(); + + const Engine = await ethers.getContractFactory("Engine"); + const engine = await Engine.deploy( + ONE_ADDRESS, + ONE_ADDRESS, + ONE_ADDRESS, + ONE_ADDRESS, + ONE_ADDRESS + ); + await engine.waitForDeployment(); + + return { engine, owner, otherAccount }; + } + + describe("Signature checks", function () { + it("The engine deployed successfully", async function () { + const { engine } = await loadFixture(bootstrapSystem); + + expect(await engine.getAddress()).to.exist; + }); + + it("Signature is verified", async () => { + const { engine } = await loadFixture(bootstrapSystem); + const wallet = new ethers.Wallet(signerPrivateKey); + const signer = wallet.address; + const engineAddress = await engine.getAddress(); + + const domain = { + name: "SMv3: OrderBook", + version: "1", + chainId: 31337, + verifyingContract: engineAddress, + }; + + const types = { + OrderDetails: [ + { name: "marketId", type: "uint128" }, + { name: "accountId", type: "uint128" }, + { name: "sizeDelta", type: "int128" }, + { name: "settlementStrategyId", type: "uint128" }, + { name: "acceptablePrice", type: "uint256" }, + { name: "isReduceOnly", type: "bool" }, + { name: "trackingCode", type: "bytes32" }, + { name: "referrer", type: "address" }, + ], + ConditionalOrder: [ + { name: "orderDetails", type: "OrderDetails" }, + { name: "signer", type: "address" }, + { name: "nonce", type: "uint256" }, + { name: "requireVerified", type: "bool" }, + { name: "trustedExecutor", type: "address" }, + { name: "maxExecutorFee", type: "uint256" }, + { name: "conditions", type: "bytes[]" }, + ], + }; + + let orderDetails = { + marketId: BigInt(marketId), + accountId: BigInt(accountId), + sizeDelta: BigInt(sizeDelta), + settlementStrategyId: BigInt(settlementStrategyId), + acceptablePrice: BigInt(acceptablePrice), + isReduceOnly: isReduceOnly, + trackingCode: trackingCode, + referrer: referrer, + }; + + // define the conditional order struct + let conditionalOrder = { + orderDetails: orderDetails, + signer: signer, + nonce: BigInt(nonce), + requireVerified: requireVerified, + trustedExecutor: trustedExecutor, + maxExecutorFee: BigInt(maxExecutorFee), + conditions: conditions, + }; + + const signature = await wallet.signTypedData( + domain, + types, + conditionalOrder + ); + + const recoveredAddress = ethers.verifyTypedData( + domain, + types, + conditionalOrder, + signature + ); + + expect(recoveredAddress).to.equal(signer); + + const res = await engine.verifySignature(conditionalOrder, signature); + expect(res).to.be.true; + }); + }); +}); diff --git a/test/utils/ConditionalOrderSignature.sol b/test/utils/ConditionalOrderSignature.sol index 7b159c0a..a7f8f330 100644 --- a/test/utils/ConditionalOrderSignature.sol +++ b/test/utils/ConditionalOrderSignature.sol @@ -39,7 +39,7 @@ contract ConditionalOrderSignature { // array of dynamic length bytes must be hashed separately // to create an array of fixed length bytes32 hashes - bytes32[] memory hashedConditions; + bytes32[] memory hashedConditions = new bytes32[](co.conditions.length); for (uint256 i = 0; i < co.conditions.length; i++) { hashedConditions[i] = keccak256(co.conditions[i]); } @@ -53,7 +53,7 @@ contract ConditionalOrderSignature { co.requireVerified, co.trustedExecutor, co.maxExecutorFee, - keccak256(abi.encode(hashedConditions)) + keccak256(abi.encodePacked(hashedConditions)) ) ); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..574e785c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true + } +}