From e63795b3b7fd98b135f47dbf3e5861264ae3e4a8 Mon Sep 17 00:00:00 2001 From: Mikhail Melnik Date: Thu, 20 Jun 2024 13:09:09 +0800 Subject: [PATCH 01/13] create OrderRegistrator --- contracts/helpers/OrderRegistrator.sol | 42 ++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 contracts/helpers/OrderRegistrator.sol diff --git a/contracts/helpers/OrderRegistrator.sol b/contracts/helpers/OrderRegistrator.sol new file mode 100644 index 00000000..895ed47a --- /dev/null +++ b/contracts/helpers/OrderRegistrator.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.23; + +import { Address, AddressLib } from "@1inch/solidity-utils/contracts/libraries/AddressLib.sol"; +import { ECDSA } from "@1inch/solidity-utils/contracts/libraries/ECDSA.sol"; +import { IOrderMixin } from "../interfaces/IOrderMixin.sol"; +import { OrderLib } from "../OrderLib.sol"; + +contract OrderRegistrator { + using AddressLib for Address; + using OrderLib for IOrderMixin.Order; + + error BadSignature(); + + event OrderRegistered(IOrderMixin.Order order, bytes extension, bytes signature); + + IOrderMixin private immutable _LIMIT_ORDER_PROTOCOL; + + constructor(IOrderMixin limitOrderProtocol) { + _LIMIT_ORDER_PROTOCOL = limitOrderProtocol; + } + + function registerOrder(IOrderMixin.Order calldata order, bytes calldata extension, bytes calldata signature) external { + // Validate order + { + (bool valid, bytes4 validationResult) = order.isValidExtension(extension); + if (!valid) { + // solhint-disable-next-line no-inline-assembly + assembly ("memory-safe") { + mstore(0, validationResult) + revert(0, 4) + } + } + } + + // Validate signature + if(!ECDSA.recoverOrIsValidSignature(order.maker.get(), _LIMIT_ORDER_PROTOCOL.hashOrder(order), signature)) revert BadSignature(); + + emit OrderRegistered(order, extension, signature); + } +} From f6b8d183ec5d6bd8d3e01596f0a83222a51ccde8 Mon Sep 17 00:00:00 2001 From: Mikhail Melnik Date: Thu, 20 Jun 2024 13:12:44 +0800 Subject: [PATCH 02/13] add interface --- contracts/helpers/OrderRegistrator.sol | 7 ++----- contracts/interfaces/IOrderRegistrator.sol | 13 +++++++++++++ 2 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 contracts/interfaces/IOrderRegistrator.sol diff --git a/contracts/helpers/OrderRegistrator.sol b/contracts/helpers/OrderRegistrator.sol index 895ed47a..4b4b502b 100644 --- a/contracts/helpers/OrderRegistrator.sol +++ b/contracts/helpers/OrderRegistrator.sol @@ -5,16 +5,13 @@ pragma solidity 0.8.23; import { Address, AddressLib } from "@1inch/solidity-utils/contracts/libraries/AddressLib.sol"; import { ECDSA } from "@1inch/solidity-utils/contracts/libraries/ECDSA.sol"; import { IOrderMixin } from "../interfaces/IOrderMixin.sol"; +import { IOrderRegistrator } from "../interfaces/IOrderRegistrator.sol"; import { OrderLib } from "../OrderLib.sol"; -contract OrderRegistrator { +contract OrderRegistrator is IOrderRegistrator { using AddressLib for Address; using OrderLib for IOrderMixin.Order; - error BadSignature(); - - event OrderRegistered(IOrderMixin.Order order, bytes extension, bytes signature); - IOrderMixin private immutable _LIMIT_ORDER_PROTOCOL; constructor(IOrderMixin limitOrderProtocol) { diff --git a/contracts/interfaces/IOrderRegistrator.sol b/contracts/interfaces/IOrderRegistrator.sol new file mode 100644 index 00000000..e383db1f --- /dev/null +++ b/contracts/interfaces/IOrderRegistrator.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.23; + +import { IOrderMixin } from "./IOrderMixin.sol"; + +interface IOrderRegistrator { + error BadSignature(); + + event OrderRegistered(IOrderMixin.Order order, bytes extension, bytes signature); + + function registerOrder(IOrderMixin.Order calldata order, bytes calldata extension, bytes calldata signature) external; +} From f352d28be2bbbb9ed730837589d7ac950a32f8f3 Mon Sep 17 00:00:00 2001 From: Mikhail Melnik Date: Thu, 20 Jun 2024 13:12:59 +0800 Subject: [PATCH 03/13] bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a74ad370..8da39d26 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@1inch/limit-order-protocol-contract", - "version": "4.0.3", + "version": "4.0.4", "description": "1inch Limit Order Protocol", "repository": { "type": "git", From 20337cd89194cf929f5b77eb3d48a5a1821b272d Mon Sep 17 00:00:00 2001 From: Mikhail Melnik Date: Thu, 20 Jun 2024 18:06:36 +0800 Subject: [PATCH 04/13] add SafeOrderBuilder --- contracts/helpers/SafeOrderBuilder.sol | 67 ++++++++++++++++++++++++++ package.json | 1 + yarn.lock | 5 ++ 3 files changed, 73 insertions(+) create mode 100644 contracts/helpers/SafeOrderBuilder.sol diff --git a/contracts/helpers/SafeOrderBuilder.sol b/contracts/helpers/SafeOrderBuilder.sol new file mode 100644 index 00000000..78cb19e9 --- /dev/null +++ b/contracts/helpers/SafeOrderBuilder.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.23; + +import { AggregatorV3Interface } from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; +import { GnosisSafeStorage } from "@gnosis.pm/safe-contracts/contracts/examples/libraries/GnosisSafeStorage.sol"; +import { GnosisSafe } from "@gnosis.pm/safe-contracts/contracts/GnosisSafe.sol"; +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; +import { EIP712 } from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; +import { IOrderMixin } from "../interfaces/IOrderMixin.sol"; +import { IOrderRegistrator } from "../interfaces/IOrderRegistrator.sol"; + +contract SafeOrderBuilder is GnosisSafeStorage { + error StaleOraclePrice(); + + bytes32 private constant SAFE_MSG_TYPEHASH = keccak256("SafeMessage(bytes message)"); + + IOrderMixin private immutable _LIMIT_ORDER_PROTOCOL; + IOrderRegistrator private immutable _ORDER_REGISTRATOR; + + constructor(IOrderMixin limitOrderProtocol, IOrderRegistrator orderRegistrator) { + _LIMIT_ORDER_PROTOCOL = limitOrderProtocol; + _ORDER_REGISTRATOR = orderRegistrator; + } + + struct OracleQueryParams { + AggregatorV3Interface oracle; + uint256 originalAnswer; + uint256 ttl; + } + + function buildAndSignOrder( + IOrderMixin.Order memory order, + bytes calldata extension, + OracleQueryParams calldata makerAssetOracleParams, + OracleQueryParams calldata takerAssetOracleParams + ) external { + { + // account for makerAsset volatility + (, int256 latestAnswer,, uint256 updatedAt,) = makerAssetOracleParams.oracle.latestRoundData(); + // solhint-disable-next-line not-rely-on-time + if (updatedAt + makerAssetOracleParams.ttl < block.timestamp) revert StaleOraclePrice(); + order.takingAmount = Math.mulDiv(order.takingAmount, uint256(latestAnswer), makerAssetOracleParams.originalAnswer); + } + + { + // account for takerAsset volatility + (, int256 latestAnswer,, uint256 updatedAt,) = takerAssetOracleParams.oracle.latestRoundData(); + // solhint-disable-next-line not-rely-on-time + if (updatedAt + takerAssetOracleParams.ttl < block.timestamp) revert StaleOraclePrice(); + order.takingAmount = Math.mulDiv(order.takingAmount, takerAssetOracleParams.originalAnswer, uint256(latestAnswer)); + } + + bytes32 msgHash = _getMessageHash(abi.encode(_LIMIT_ORDER_PROTOCOL.hashOrder(order))); + signedMessages[msgHash] = 1; + + _ORDER_REGISTRATOR.registerOrder(order, extension, ""); + } + + /// @dev Returns hash of a message that can be signed by owners. + /// @param message Message that should be hashed + /// @return Message hash. + function _getMessageHash(bytes memory message) private view returns (bytes32) { + bytes32 safeMessageHash = keccak256(abi.encode(SAFE_MSG_TYPEHASH, keccak256(message))); + return keccak256(abi.encodePacked(bytes1(0x19), bytes1(0x01), GnosisSafe(payable(address(this))).domainSeparator(), safeMessageHash)); + } +} diff --git a/package.json b/package.json index 8da39d26..3788783d 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "dependencies": { "@1inch/solidity-utils": "4.2.1", "@chainlink/contracts": "0.8.0", + "@gnosis.pm/safe-contracts": "1.3.0", "@openzeppelin/contracts": "5.0.1" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index d9b3fea1..00e747b8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -820,6 +820,11 @@ resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.0.tgz#0709e9f4cb252351c609c6e6d8d6779a8d25edff" integrity sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA== +"@gnosis.pm/safe-contracts@1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@gnosis.pm/safe-contracts/-/safe-contracts-1.3.0.tgz#316741a7690d8751a1f701538cfc9ec80866eedc" + integrity sha512-1p+1HwGvxGUVzVkFjNzglwHrLNA67U/axP0Ct85FzzH8yhGJb4t9jDjPYocVMzLorDoWAfKicGy1akPY9jXRVw== + "@humanwhocodes/config-array@^0.11.13": version "0.11.14" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" From 92115b742f6374c208192837611c0b506a5819c7 Mon Sep 17 00:00:00 2001 From: Mikhail Melnik Date: Thu, 20 Jun 2024 18:07:19 +0800 Subject: [PATCH 05/13] linter --- contracts/helpers/SafeOrderBuilder.sol | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/contracts/helpers/SafeOrderBuilder.sol b/contracts/helpers/SafeOrderBuilder.sol index 78cb19e9..86c4a217 100644 --- a/contracts/helpers/SafeOrderBuilder.sol +++ b/contracts/helpers/SafeOrderBuilder.sol @@ -6,14 +6,13 @@ import { AggregatorV3Interface } from "@chainlink/contracts/src/v0.8/interfaces/ import { GnosisSafeStorage } from "@gnosis.pm/safe-contracts/contracts/examples/libraries/GnosisSafeStorage.sol"; import { GnosisSafe } from "@gnosis.pm/safe-contracts/contracts/GnosisSafe.sol"; import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; -import { EIP712 } from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; import { IOrderMixin } from "../interfaces/IOrderMixin.sol"; import { IOrderRegistrator } from "../interfaces/IOrderRegistrator.sol"; contract SafeOrderBuilder is GnosisSafeStorage { error StaleOraclePrice(); - bytes32 private constant SAFE_MSG_TYPEHASH = keccak256("SafeMessage(bytes message)"); + bytes32 private constant _SAFE_MSG_TYPEHASH = keccak256("SafeMessage(bytes message)"); IOrderMixin private immutable _LIMIT_ORDER_PROTOCOL; IOrderRegistrator private immutable _ORDER_REGISTRATOR; @@ -61,7 +60,7 @@ contract SafeOrderBuilder is GnosisSafeStorage { /// @param message Message that should be hashed /// @return Message hash. function _getMessageHash(bytes memory message) private view returns (bytes32) { - bytes32 safeMessageHash = keccak256(abi.encode(SAFE_MSG_TYPEHASH, keccak256(message))); + bytes32 safeMessageHash = keccak256(abi.encode(_SAFE_MSG_TYPEHASH, keccak256(message))); return keccak256(abi.encodePacked(bytes1(0x19), bytes1(0x01), GnosisSafe(payable(address(this))).domainSeparator(), safeMessageHash)); } } From 6cde1ecfbd35af36266c9aa7be654254ca4bf01e Mon Sep 17 00:00:00 2001 From: Mikhail Melnik Date: Tue, 25 Jun 2024 16:22:16 +0800 Subject: [PATCH 06/13] add OrderRegistrator tests --- contracts/helpers/OrderRegistrator.sol | 2 +- contracts/interfaces/IOrderRegistrator.sol | 2 - test/OrderRegistrator.js | 76 ++++++++++++++++++++++ 3 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 test/OrderRegistrator.js diff --git a/contracts/helpers/OrderRegistrator.sol b/contracts/helpers/OrderRegistrator.sol index 4b4b502b..b800af33 100644 --- a/contracts/helpers/OrderRegistrator.sol +++ b/contracts/helpers/OrderRegistrator.sol @@ -32,7 +32,7 @@ contract OrderRegistrator is IOrderRegistrator { } // Validate signature - if(!ECDSA.recoverOrIsValidSignature(order.maker.get(), _LIMIT_ORDER_PROTOCOL.hashOrder(order), signature)) revert BadSignature(); + if(!ECDSA.recoverOrIsValidSignature(order.maker.get(), _LIMIT_ORDER_PROTOCOL.hashOrder(order), signature)) revert IOrderMixin.BadSignature(); emit OrderRegistered(order, extension, signature); } diff --git a/contracts/interfaces/IOrderRegistrator.sol b/contracts/interfaces/IOrderRegistrator.sol index e383db1f..bc8768ea 100644 --- a/contracts/interfaces/IOrderRegistrator.sol +++ b/contracts/interfaces/IOrderRegistrator.sol @@ -5,8 +5,6 @@ pragma solidity 0.8.23; import { IOrderMixin } from "./IOrderMixin.sol"; interface IOrderRegistrator { - error BadSignature(); - event OrderRegistered(IOrderMixin.Order order, bytes extension, bytes signature); function registerOrder(IOrderMixin.Order calldata order, bytes calldata extension, bytes calldata signature) external; diff --git a/test/OrderRegistrator.js b/test/OrderRegistrator.js new file mode 100644 index 00000000..7628f2fc --- /dev/null +++ b/test/OrderRegistrator.js @@ -0,0 +1,76 @@ +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { expect } = require('@1inch/solidity-utils'); +const { signOrder, buildOrder } = require('./helpers/orderUtils'); +const { ethers } = require('hardhat'); +const { deploySwap, deployUSDC, deployUSDT } = require('./helpers/fixtures'); + +describe('OrderRegistrator', function () { + let addr; + + before(async function () { + [addr] = await ethers.getSigners(); + }); + + async function deployAndInit () { + const { swap } = await deploySwap(); + const { usdc } = await deployUSDC(); + const { usdt } = await deployUSDT(); + const OrderRegistrator = await ethers.getContractFactory('OrderRegistrator'); + const registrator = await OrderRegistrator.deploy(swap); + await registrator.waitForDeployment(); + const chainId = (await ethers.provider.getNetwork()).chainId; + return { swap, usdc, usdt, registrator, chainId }; + }; + + it('should emit OrderRegistered event', async function () { + const { usdc, usdt, swap, registrator, chainId } = await loadFixture(deployAndInit); + + const order = buildOrder({ + makerAsset: await usdc.getAddress(), + takerAsset: await usdt.getAddress(), + makingAmount: 1, + takingAmount: 2, + maker: addr.address, + }); + + const orderTuple = [order.salt, order.maker, order.receiver, order.makerAsset, order.takerAsset, order.makingAmount, order.takingAmount, order.makerTraits]; + + const signature = ethers.Signature.from(await signOrder(order, chainId, await swap.getAddress(), addr)).compactSerialized; + + const tx = registrator.registerOrder(order, order.extension, signature); + await expect(tx).to.emit(registrator, 'OrderRegistered').withArgs(orderTuple, order.extension, signature); + }); + + it('should revert with wrong signature', async function () { + const { usdc, usdt, swap, registrator, chainId } = await loadFixture(deployAndInit); + + const order = buildOrder({ + makerAsset: await usdc.getAddress(), + takerAsset: await usdt.getAddress(), + makingAmount: 1, + takingAmount: 2, + maker: addr.address, + }); + const signature = ethers.Signature.from(await signOrder(order, chainId + 1n, await swap.getAddress(), addr)).compactSerialized; + + const tx = registrator.registerOrder(order, order.extension, signature); + await expect(tx).to.be.revertedWithCustomError(swap, 'BadSignature'); + }); + + it('should revert with wrong extension', async function () { + const { usdc, usdt, swap, registrator, chainId } = await loadFixture(deployAndInit); + + const order = buildOrder({ + makerAsset: await usdc.getAddress(), + takerAsset: await usdt.getAddress(), + makingAmount: 1, + takingAmount: 2, + maker: addr.address, + }); + const orderLibFactory = await ethers.getContractFactory('OrderLib'); + + const signature = ethers.Signature.from(await signOrder(order, chainId, await swap.getAddress(), addr)).compactSerialized; + const tx = registrator.registerOrder(order, order.extension + '00', signature); + await expect(tx).to.be.revertedWithCustomError(orderLibFactory, 'UnexpectedOrderExtension'); + }); +}); From 6bf22b3631d2aa1700155b28f1695169a5b203e2 Mon Sep 17 00:00:00 2001 From: Mikhail Melnik Date: Thu, 27 Jun 2024 17:33:17 +0800 Subject: [PATCH 07/13] add SafeOrderBuilder tests --- hardhat.config.js | 2 + test/SafeOrderBuilder.js | 116 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 test/SafeOrderBuilder.js diff --git a/hardhat.config.js b/hardhat.config.js index 232f3920..5f75dea1 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -43,6 +43,8 @@ module.exports = { paths: [ '@1inch/solidity-utils/contracts/mocks/TokenCustomDecimalsMock.sol', '@1inch/solidity-utils/contracts/mocks/TokenMock.sol', + '@gnosis.pm/safe-contracts/contracts/proxies/GnosisSafeProxyFactory.sol', + '@gnosis.pm/safe-contracts/contracts/handler/CompatibilityFallbackHandler.sol', ], }, zksolc: { diff --git a/test/SafeOrderBuilder.js b/test/SafeOrderBuilder.js new file mode 100644 index 00000000..cedd9f8c --- /dev/null +++ b/test/SafeOrderBuilder.js @@ -0,0 +1,116 @@ +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { expect, constants, ether } = require('@1inch/solidity-utils'); +const { buildOrder, buildTakerTraits } = require('./helpers/orderUtils'); +const { ethers } = require('hardhat'); +const { deploySwap, deployUSDC, deployUSDT } = require('./helpers/fixtures'); +const { executeContractCallWithSigners } = require('@gnosis.pm/safe-contracts/dist'); + +describe('SafeOrderBuilder', function () { + let addr; + + before(async function () { + [addr] = await ethers.getSigners(); + }); + + async function deployAndInit () { + const { swap } = await deploySwap(); + const { usdc } = await deployUSDC(); + const { usdt } = await deployUSDT(); + + const OrderRegistrator = await ethers.getContractFactory('OrderRegistrator'); + const registrator = await OrderRegistrator.deploy(swap); + await registrator.waitForDeployment(); + const chainId = (await ethers.provider.getNetwork()).chainId; + + const GnosisSafeProxyFactory = await ethers.getContractFactory('GnosisSafeProxyFactory'); + const proxyFactoryContract = await GnosisSafeProxyFactory.deploy(); + await proxyFactoryContract.waitForDeployment(); + const GnosisSafe = await ethers.getContractFactory('GnosisSafe'); + const gnosisSafeContract = await GnosisSafe.deploy(); + await gnosisSafeContract.waitForDeployment(); + const CompatibilityFallbackHandler = await ethers.getContractFactory('CompatibilityFallbackHandler'); + const fallbackHandler = await CompatibilityFallbackHandler.deploy(); + await fallbackHandler.waitForDeployment(); + const SafeOrderBuilder = await ethers.getContractFactory('SafeOrderBuilder'); + const safeOrderBuilder = await SafeOrderBuilder.deploy(swap, registrator); + await safeOrderBuilder.waitForDeployment(); + const AggregatorMock = await ethers.getContractFactory('AggregatorMock'); + const usdcOracle = await AggregatorMock.deploy(ether('0.00025')); + await usdcOracle.waitForDeployment(); + const usdtOracle = await AggregatorMock.deploy(ether('0.00025')); + await usdtOracle.waitForDeployment(); + + const owner1SafeData = await gnosisSafeContract.interface.encodeFunctionData( + 'setup', + [[addr.address], 1, constants.ZERO_ADDRESS, '0x', await fallbackHandler.getAddress(), constants.ZERO_ADDRESS, 0, constants.ZERO_ADDRESS], + ); + + const txn = await proxyFactoryContract.createProxy(await gnosisSafeContract.getAddress(), owner1SafeData); + const receipt = await txn.wait(); + const safe = await GnosisSafe.attach(receipt.logs[1].args[0]); + + // workaround as safe lib expects old version of ethers + safe.address = await safe.getAddress(); + safeOrderBuilder.address = await safeOrderBuilder.getAddress(); + usdc.address = await usdc.getAddress(); + + const order = buildOrder({ + makerAsset: await usdc.getAddress(), + takerAsset: await usdt.getAddress(), + makingAmount: 100n, + takingAmount: 100n, + maker: await safe.getAddress(), + }); + + await usdc.mint(await safe.getAddress(), 1000n); + await usdt.mint(addr.address, 1000n); + await usdt.approve(await swap.getAddress(), 1000n); + await executeContractCallWithSigners( + safe, + usdc, + 'approve', + [await swap.getAddress(), 1000n], + [addr], + false, + ); + + return { swap, usdc, usdt, registrator, chainId, safe, safeOrderBuilder, usdcOracle, usdtOracle, order }; + }; + + const testCases = [ + [ether('0.00025'), ether('0.00025'), 1n, 1n], + [ether('0.0002'), ether('0.00025'), 5n, 4n], + [ether('0.00025'), ether('0.0002'), 4n, 5n], + [ether('0.00025'), ether('0.0005'), 2n, 1n], + [ether('0.0005'), ether('0.00025'), 1n, 2n], + [ether('0.0003'), ether('0.0002'), 2n, 3n], + ]; + + for (const [makerOracleResult, takerOracleResult, numerator, denominator] of testCases) { + const testName = `price change ${Number(100n * ether('0.00025') / makerOracleResult) / 100} ${Number(100n * ether('0.00025') / takerOracleResult) / 100}`; + it(testName, async function () { + const { swap, safe, registrator, safeOrderBuilder, usdcOracle, usdtOracle, order } = await loadFixture(deployAndInit); + + const tx = await executeContractCallWithSigners( + safe, + safeOrderBuilder, + 'buildAndSignOrder', + [order, order.extension, [await usdcOracle.getAddress(), makerOracleResult, 1000], [await usdtOracle.getAddress(), takerOracleResult, 1000]], + [addr], + true, + ); + + order.takingAmount = order.takingAmount * numerator / denominator; + + const orderTuple = [order.salt, order.maker, order.receiver, order.makerAsset, order.takerAsset, order.makingAmount, order.takingAmount, order.makerTraits]; + await expect(tx).to.emit(registrator, 'OrderRegistered').withArgs(orderTuple, order.extension, '0x'); + + const takerTraits = buildTakerTraits({ + makingAmount: true, + threshold: 1000, + }); + + await swap.fillContractOrder(order, '0x', order.makingAmount, takerTraits.traits); + }); + } +}); From 1f26b3e71d8595d50fdbd44d23c999c5d1b06e28 Mon Sep 17 00:00:00 2001 From: Mikhail Melnik Date: Thu, 27 Jun 2024 17:34:17 +0800 Subject: [PATCH 08/13] bump minor version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3788783d..c90e8130 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@1inch/limit-order-protocol-contract", - "version": "4.0.4", + "version": "4.1.0", "description": "1inch Limit Order Protocol", "repository": { "type": "git", From dc56aa1898832dc5bcf898af2ca5235ee8f94173 Mon Sep 17 00:00:00 2001 From: Mikhail Melnik Date: Thu, 27 Jun 2024 17:41:25 +0800 Subject: [PATCH 09/13] fix --- .../mocks/CompatibilityFallbackHandler.sol | 148 ++++++++++++++++++ hardhat.config.js | 1 - 2 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 contracts/mocks/CompatibilityFallbackHandler.sol diff --git a/contracts/mocks/CompatibilityFallbackHandler.sol b/contracts/mocks/CompatibilityFallbackHandler.sol new file mode 100644 index 00000000..908607aa --- /dev/null +++ b/contracts/mocks/CompatibilityFallbackHandler.sol @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.7.0 <0.9.0; + +// This mock is needed because gnosis CompatibilityFallbackHandler.sol does not compile with modern solidity version +// isValidSignature changes argument types location from memory to calldata which is not allowed +// TODO: switch to original version when new version of @gnosis.pm will be released + +import "@gnosis.pm/safe-contracts/contracts/handler/DefaultCallbackHandler.sol"; +import "@gnosis.pm/safe-contracts/contracts/interfaces/ISignatureValidator.sol"; +import "@gnosis.pm/safe-contracts/contracts/GnosisSafe.sol"; + +/// @title Compatibility Fallback Handler - fallback handler to provider compatibility between pre 1.3.0 and 1.3.0+ Safe contracts +/// @author Richard Meissner - +contract CompatibilityFallbackHandler is DefaultCallbackHandler, ISignatureValidator { + //keccak256( + // "SafeMessage(bytes message)" + //); + bytes32 private constant SAFE_MSG_TYPEHASH = 0x60b3cbf8b4a223d68d641b3b6ddf9a298e7f33710cf3d3a9d1146b5a6150fbca; + + bytes4 internal constant SIMULATE_SELECTOR = bytes4(keccak256("simulate(address,bytes)")); + + address internal constant SENTINEL_MODULES = address(0x1); + bytes4 internal constant UPDATED_MAGIC_VALUE = 0x1626ba7e; + + /** + * Implementation of ISignatureValidator (see `interfaces/ISignatureValidator.sol`) + * @dev Should return whether the signature provided is valid for the provided data. + * @param _data Arbitrary length data signed on the behalf of address(msg.sender) + * @param _signature Signature byte array associated with _data + * @return a bool upon valid or invalid signature with corresponding _data + */ + function isValidSignature(bytes memory _data, bytes memory _signature) public view override returns (bytes4) { + // Caller should be a Safe + GnosisSafe safe = GnosisSafe(payable(msg.sender)); + bytes32 messageHash = getMessageHashForSafe(safe, _data); + if (_signature.length == 0) { + require(safe.signedMessages(messageHash) != 0, "Hash not approved"); + } else { + safe.checkSignatures(messageHash, _data, _signature); + } + return EIP1271_MAGIC_VALUE; + } + + /// @dev Returns hash of a message that can be signed by owners. + /// @param message Message that should be hashed + /// @return Message hash. + function getMessageHash(bytes memory message) public view returns (bytes32) { + return getMessageHashForSafe(GnosisSafe(payable(msg.sender)), message); + } + + /// @dev Returns hash of a message that can be signed by owners. + /// @param safe Safe to which the message is targeted + /// @param message Message that should be hashed + /// @return Message hash. + function getMessageHashForSafe(GnosisSafe safe, bytes memory message) public view returns (bytes32) { + bytes32 safeMessageHash = keccak256(abi.encode(SAFE_MSG_TYPEHASH, keccak256(message))); + return keccak256(abi.encodePacked(bytes1(0x19), bytes1(0x01), safe.domainSeparator(), safeMessageHash)); + } + + /** + * Implementation of updated EIP-1271 + * @dev Should return whether the signature provided is valid for the provided data. + * The save does not implement the interface since `checkSignatures` is not a view method. + * The method will not perform any state changes (see parameters of `checkSignatures`) + * @param _dataHash Hash of the data signed on the behalf of address(msg.sender) + * @param _signature Signature byte array associated with _dataHash + * @return a bool upon valid or invalid signature with corresponding _dataHash + * @notice See https://github.com/gnosis/util-contracts/blob/bb5fe5fb5df6d8400998094fb1b32a178a47c3a1/contracts/StorageAccessible.sol + */ + function isValidSignature(bytes32 _dataHash, bytes calldata _signature) external view returns (bytes4) { + ISignatureValidator validator = ISignatureValidator(msg.sender); + bytes4 value = validator.isValidSignature(abi.encode(_dataHash), _signature); + return (value == EIP1271_MAGIC_VALUE) ? UPDATED_MAGIC_VALUE : bytes4(0); + } + + /// @dev Returns array of first 10 modules. + /// @return Array of modules. + function getModules() external view returns (address[] memory) { + // Caller should be a Safe + GnosisSafe safe = GnosisSafe(payable(msg.sender)); + (address[] memory array, ) = safe.getModulesPaginated(SENTINEL_MODULES, 10); + return array; + } + + /** + * @dev Performs a delegetecall on a targetContract in the context of self. + * Internally reverts execution to avoid side effects (making it static). Catches revert and returns encoded result as bytes. + * @param targetContract Address of the contract containing the code to execute. + * @param calldataPayload Calldata that should be sent to the target contract (encoded method name and arguments). + */ + function simulate(address targetContract, bytes calldata calldataPayload) external returns (bytes memory response) { + // Suppress compiler warnings about not using parameters, while allowing + // parameters to keep names for documentation purposes. This does not + // generate code. + targetContract; + calldataPayload; + + // solhint-disable-next-line no-inline-assembly + assembly { + let internalCalldata := mload(0x40) + // Store `simulateAndRevert.selector`. + // String representation is used to force right padding + mstore(internalCalldata, "\xb4\xfa\xba\x09") + // Abuse the fact that both this and the internal methods have the + // same signature, and differ only in symbol name (and therefore, + // selector) and copy calldata directly. This saves us approximately + // 250 bytes of code and 300 gas at runtime over the + // `abi.encodeWithSelector` builtin. + calldatacopy(add(internalCalldata, 0x04), 0x04, sub(calldatasize(), 0x04)) + + // `pop` is required here by the compiler, as top level expressions + // can't have return values in inline assembly. `call` typically + // returns a 0 or 1 value indicated whether or not it reverted, but + // since we know it will always revert, we can safely ignore it. + pop( + call( + gas(), + // address() has been changed to caller() to use the implementation of the Safe + caller(), + 0, + internalCalldata, + calldatasize(), + // The `simulateAndRevert` call always reverts, and + // instead encodes whether or not it was successful in the return + // data. The first 32-byte word of the return data contains the + // `success` value, so write it to memory address 0x00 (which is + // reserved Solidity scratch space and OK to use). + 0x00, + 0x20 + ) + ) + + // Allocate and copy the response bytes, making sure to increment + // the free memory pointer accordingly (in case this method is + // called as an internal function). The remaining `returndata[0x20:]` + // contains the ABI encoded response bytes, so we can just write it + // as is to memory. + let responseSize := sub(returndatasize(), 0x20) + response := mload(0x40) + mstore(0x40, add(response, responseSize)) + returndatacopy(response, 0x20, responseSize) + + if iszero(mload(0x00)) { + revert(add(response, 0x20), mload(response)) + } + } + } +} diff --git a/hardhat.config.js b/hardhat.config.js index 5f75dea1..9d4759ba 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -44,7 +44,6 @@ module.exports = { '@1inch/solidity-utils/contracts/mocks/TokenCustomDecimalsMock.sol', '@1inch/solidity-utils/contracts/mocks/TokenMock.sol', '@gnosis.pm/safe-contracts/contracts/proxies/GnosisSafeProxyFactory.sol', - '@gnosis.pm/safe-contracts/contracts/handler/CompatibilityFallbackHandler.sol', ], }, zksolc: { From ab9d231236f82397725bdcedbf20bd763a82f1f0 Mon Sep 17 00:00:00 2001 From: Mikhail Melnik Date: Thu, 27 Jun 2024 17:45:20 +0800 Subject: [PATCH 10/13] linter --- .../mocks/CompatibilityFallbackHandler.sol | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/contracts/mocks/CompatibilityFallbackHandler.sol b/contracts/mocks/CompatibilityFallbackHandler.sol index 908607aa..2bda9d6c 100644 --- a/contracts/mocks/CompatibilityFallbackHandler.sol +++ b/contracts/mocks/CompatibilityFallbackHandler.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-only -pragma solidity >=0.7.0 <0.9.0; +pragma solidity ^0.8.0; // This mock is needed because gnosis CompatibilityFallbackHandler.sol does not compile with modern solidity version // isValidSignature changes argument types location from memory to calldata which is not allowed @@ -15,12 +15,12 @@ contract CompatibilityFallbackHandler is DefaultCallbackHandler, ISignatureValid //keccak256( // "SafeMessage(bytes message)" //); - bytes32 private constant SAFE_MSG_TYPEHASH = 0x60b3cbf8b4a223d68d641b3b6ddf9a298e7f33710cf3d3a9d1146b5a6150fbca; + bytes32 private constant _SAFE_MSG_TYPEHASH = 0x60b3cbf8b4a223d68d641b3b6ddf9a298e7f33710cf3d3a9d1146b5a6150fbca; - bytes4 internal constant SIMULATE_SELECTOR = bytes4(keccak256("simulate(address,bytes)")); + bytes4 internal constant _SIMULATE_SELECTOR = bytes4(keccak256("simulate(address,bytes)")); - address internal constant SENTINEL_MODULES = address(0x1); - bytes4 internal constant UPDATED_MAGIC_VALUE = 0x1626ba7e; + address internal constant _SENTINEL_MODULES = address(0x1); + bytes4 internal constant _UPDATED_MAGIC_VALUE = 0x1626ba7e; /** * Implementation of ISignatureValidator (see `interfaces/ISignatureValidator.sol`) @@ -34,6 +34,7 @@ contract CompatibilityFallbackHandler is DefaultCallbackHandler, ISignatureValid GnosisSafe safe = GnosisSafe(payable(msg.sender)); bytes32 messageHash = getMessageHashForSafe(safe, _data); if (_signature.length == 0) { + // solhint-disable-next-line custom-errors require(safe.signedMessages(messageHash) != 0, "Hash not approved"); } else { safe.checkSignatures(messageHash, _data, _signature); @@ -53,7 +54,7 @@ contract CompatibilityFallbackHandler is DefaultCallbackHandler, ISignatureValid /// @param message Message that should be hashed /// @return Message hash. function getMessageHashForSafe(GnosisSafe safe, bytes memory message) public view returns (bytes32) { - bytes32 safeMessageHash = keccak256(abi.encode(SAFE_MSG_TYPEHASH, keccak256(message))); + bytes32 safeMessageHash = keccak256(abi.encode(_SAFE_MSG_TYPEHASH, keccak256(message))); return keccak256(abi.encodePacked(bytes1(0x19), bytes1(0x01), safe.domainSeparator(), safeMessageHash)); } @@ -70,7 +71,7 @@ contract CompatibilityFallbackHandler is DefaultCallbackHandler, ISignatureValid function isValidSignature(bytes32 _dataHash, bytes calldata _signature) external view returns (bytes4) { ISignatureValidator validator = ISignatureValidator(msg.sender); bytes4 value = validator.isValidSignature(abi.encode(_dataHash), _signature); - return (value == EIP1271_MAGIC_VALUE) ? UPDATED_MAGIC_VALUE : bytes4(0); + return (value == EIP1271_MAGIC_VALUE) ? _UPDATED_MAGIC_VALUE : bytes4(0); } /// @dev Returns array of first 10 modules. @@ -78,7 +79,7 @@ contract CompatibilityFallbackHandler is DefaultCallbackHandler, ISignatureValid function getModules() external view returns (address[] memory) { // Caller should be a Safe GnosisSafe safe = GnosisSafe(payable(msg.sender)); - (address[] memory array, ) = safe.getModulesPaginated(SENTINEL_MODULES, 10); + (address[] memory array, ) = safe.getModulesPaginated(_SENTINEL_MODULES, 10); return array; } From b1a017904df27489fd54c68428655181c6db1625 Mon Sep 17 00:00:00 2001 From: Mikhail Melnik Date: Thu, 27 Jun 2024 17:49:02 +0800 Subject: [PATCH 11/13] fix --- test/SafeOrderBuilder.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/SafeOrderBuilder.js b/test/SafeOrderBuilder.js index cedd9f8c..2dd5b230 100644 --- a/test/SafeOrderBuilder.js +++ b/test/SafeOrderBuilder.js @@ -50,9 +50,12 @@ describe('SafeOrderBuilder', function () { const safe = await GnosisSafe.attach(receipt.logs[1].args[0]); // workaround as safe lib expects old version of ethers + // TODO: remove when safe lib is updated safe.address = await safe.getAddress(); safeOrderBuilder.address = await safeOrderBuilder.getAddress(); usdc.address = await usdc.getAddress(); + addr._signTypedData = addr.signTypedData; + // end of workaround const order = buildOrder({ makerAsset: await usdc.getAddress(), From 7d5f8240178cca16be6a8b9fa22aaf251342586a Mon Sep 17 00:00:00 2001 From: Mikhail Melnik Date: Fri, 28 Jun 2024 17:19:49 +0800 Subject: [PATCH 12/13] add NatSpec comments --- contracts/helpers/OrderRegistrator.sol | 6 ++++++ contracts/helpers/SafeOrderBuilder.sol | 23 +++++++++++++++++++--- contracts/interfaces/IOrderRegistrator.sol | 17 ++++++++++++++++ 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/contracts/helpers/OrderRegistrator.sol b/contracts/helpers/OrderRegistrator.sol index b800af33..4c6ee813 100644 --- a/contracts/helpers/OrderRegistrator.sol +++ b/contracts/helpers/OrderRegistrator.sol @@ -8,6 +8,9 @@ import { IOrderMixin } from "../interfaces/IOrderMixin.sol"; import { IOrderRegistrator } from "../interfaces/IOrderRegistrator.sol"; import { OrderLib } from "../OrderLib.sol"; +/** + * @title OrderRegistrator + */ contract OrderRegistrator is IOrderRegistrator { using AddressLib for Address; using OrderLib for IOrderMixin.Order; @@ -18,6 +21,9 @@ contract OrderRegistrator is IOrderRegistrator { _LIMIT_ORDER_PROTOCOL = limitOrderProtocol; } + /** + * @notice See {IOrderRegistrator-registerOrder}. + */ function registerOrder(IOrderMixin.Order calldata order, bytes calldata extension, bytes calldata signature) external { // Validate order { diff --git a/contracts/helpers/SafeOrderBuilder.sol b/contracts/helpers/SafeOrderBuilder.sol index 86c4a217..7a99f16a 100644 --- a/contracts/helpers/SafeOrderBuilder.sol +++ b/contracts/helpers/SafeOrderBuilder.sol @@ -9,6 +9,11 @@ import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; import { IOrderMixin } from "../interfaces/IOrderMixin.sol"; import { IOrderRegistrator } from "../interfaces/IOrderRegistrator.sol"; +/** + * @title SafeOrderBuilder + * @dev The contract is responsible for building and signing limit orders for the GnosisSafe. + * The contract uses oracles to adjust the order taking amount based on the volatility of the maker and taker assets. + */ contract SafeOrderBuilder is GnosisSafeStorage { error StaleOraclePrice(); @@ -28,6 +33,15 @@ contract SafeOrderBuilder is GnosisSafeStorage { uint256 ttl; } + /** + * @notice Builds and signs a limit order for the GnosisSafe. + * The order is signed by the GnosisSafe and registered in the order registrator. + * The order taking amount is adjusted based on the volatility of the maker and taker assets. + * @param order The order to be built and signed. + * @param extension The extension data associated with the order. + * @param makerAssetOracleParams The oracle query parameters for the maker asset. + * @param takerAssetOracleParams The oracle query parameters for the taker asset. + */ function buildAndSignOrder( IOrderMixin.Order memory order, bytes calldata extension, @@ -56,9 +70,12 @@ contract SafeOrderBuilder is GnosisSafeStorage { _ORDER_REGISTRATOR.registerOrder(order, extension, ""); } - /// @dev Returns hash of a message that can be signed by owners. - /// @param message Message that should be hashed - /// @return Message hash. + + /** + * @dev Returns hash of a message that can be signed by owners. + * @param message Message that should be hashed. + * @return bytes32 hash of the message. + */ function _getMessageHash(bytes memory message) private view returns (bytes32) { bytes32 safeMessageHash = keccak256(abi.encode(_SAFE_MSG_TYPEHASH, keccak256(message))); return keccak256(abi.encodePacked(bytes1(0x19), bytes1(0x01), GnosisSafe(payable(address(this))).domainSeparator(), safeMessageHash)); diff --git a/contracts/interfaces/IOrderRegistrator.sol b/contracts/interfaces/IOrderRegistrator.sol index bc8768ea..c7a2e515 100644 --- a/contracts/interfaces/IOrderRegistrator.sol +++ b/contracts/interfaces/IOrderRegistrator.sol @@ -4,8 +4,25 @@ pragma solidity 0.8.23; import { IOrderMixin } from "./IOrderMixin.sol"; +/** + * @title IOrderRegistrator + * @dev The interface defines the structure of the order registrator contract. + * The registrator is responsible for registering orders and emitting an event when an order is registered. + */ interface IOrderRegistrator { + /** + * @notice Emitted when an order is registered. + * @param order The order that was registered. + * @param extension The extension data associated with the order. + * @param signature The signature of the order. + */ event OrderRegistered(IOrderMixin.Order order, bytes extension, bytes signature); + /** + * @notice Registers an order. + * @param order The order to be registered. + * @param extension The extension data associated with the order. + * @param signature The signature of the order. + */ function registerOrder(IOrderMixin.Order calldata order, bytes calldata extension, bytes calldata signature) external; } From e8aeaa034727464b6fe3c10ca666971053b1bec0 Mon Sep 17 00:00:00 2001 From: Mikhail Melnik Date: Fri, 28 Jun 2024 17:52:18 +0800 Subject: [PATCH 13/13] add deploy script --- deploy/deploy-SafeOrderBuilder.js | 65 +++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 deploy/deploy-SafeOrderBuilder.js diff --git a/deploy/deploy-SafeOrderBuilder.js b/deploy/deploy-SafeOrderBuilder.js new file mode 100644 index 00000000..9d7fb461 --- /dev/null +++ b/deploy/deploy-SafeOrderBuilder.js @@ -0,0 +1,65 @@ +const hre = require('hardhat'); +const { ethers } = hre; +const { getChainId } = hre; + +const ROUTER_V6_ADDR = '0x111111125421ca6dc452d289314280a0f8842a65'; + +const ORDER_REGISTRATOR_SALT = ethers.keccak256(ethers.toUtf8Bytes('OrderRegistrator')); +const SAFE_ORDER_BUILDER_SALT = ethers.keccak256(ethers.toUtf8Bytes('SafeOrderBuilder')); + +const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); + +module.exports = async ({ deployments }) => { + const networkName = hre.network.name; + console.log(`running ${networkName} deploy script`); + const chainId = await getChainId(); + console.log('network id ', chainId); + if ( + networkName in hre.config.networks[networkName] && + chainId !== hre.config.networks[networkName].chainId.toString() + ) { + console.log(`network chain id: ${hre.config.networks[networkName].chainId}, your chain id ${chainId}`); + console.log('skipping wrong chain id deployment'); + return; + } + + const create3Deployer = await ethers.getContractAt('ICreate3Deployer', (await deployments.get('Create3Deployer')).address); + + const OrderRegistratorFactory = await ethers.getContractFactory('OrderRegistrator'); + + const deployData = (await OrderRegistratorFactory.getDeployTransaction(ROUTER_V6_ADDR)).data; + + const txn = create3Deployer.deploy(ORDER_REGISTRATOR_SALT, deployData, { gasLimit: 5000000 }); + await (await txn).wait(); + + const orderRegistratorAddr = await create3Deployer.addressOf(ORDER_REGISTRATOR_SALT); + + console.log('OrderRegistrator deployed to:', orderRegistratorAddr); + + const SafeOrderBuilderFactory = await ethers.getContractFactory('SafeOrderBuilder'); + + const deployData2 = (await SafeOrderBuilderFactory.getDeployTransaction(ROUTER_V6_ADDR, orderRegistratorAddr)).data; + + const txn2 = create3Deployer.deploy(SAFE_ORDER_BUILDER_SALT, deployData2, { gasLimit: 5000000 }); + await (await txn2).wait(); + + const safeOrderBuilderAddr = await create3Deployer.addressOf(SAFE_ORDER_BUILDER_SALT); + + console.log('SafeOrderBuilder deployed to:', safeOrderBuilderAddr); + + await sleep(5000); // wait for etherscan to index contract + + if (chainId !== '31337') { + await hre.run('verify:verify', { + address: orderRegistratorAddr, + constructorArguments: [ROUTER_V6_ADDR], + }); + + await hre.run('verify:verify', { + address: safeOrderBuilderAddr, + constructorArguments: [ROUTER_V6_ADDR, orderRegistratorAddr], + }); + } +}; + +module.exports.skip = async () => true;