diff --git a/contracts/helpers/OrderRegistrator.sol b/contracts/helpers/OrderRegistrator.sol new file mode 100644 index 00000000..4c6ee813 --- /dev/null +++ b/contracts/helpers/OrderRegistrator.sol @@ -0,0 +1,45 @@ +// 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 { 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; + + IOrderMixin private immutable _LIMIT_ORDER_PROTOCOL; + + constructor(IOrderMixin limitOrderProtocol) { + _LIMIT_ORDER_PROTOCOL = limitOrderProtocol; + } + + /** + * @notice See {IOrderRegistrator-registerOrder}. + */ + 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 IOrderMixin.BadSignature(); + + emit OrderRegistered(order, extension, signature); + } +} diff --git a/contracts/helpers/SafeOrderBuilder.sol b/contracts/helpers/SafeOrderBuilder.sol new file mode 100644 index 00000000..7a99f16a --- /dev/null +++ b/contracts/helpers/SafeOrderBuilder.sol @@ -0,0 +1,83 @@ +// 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 { 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(); + + 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; + } + + /** + * @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, + 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 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 new file mode 100644 index 00000000..c7a2e515 --- /dev/null +++ b/contracts/interfaces/IOrderRegistrator.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT + +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; +} diff --git a/contracts/mocks/CompatibilityFallbackHandler.sol b/contracts/mocks/CompatibilityFallbackHandler.sol new file mode 100644 index 00000000..2bda9d6c --- /dev/null +++ b/contracts/mocks/CompatibilityFallbackHandler.sol @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: LGPL-3.0-only +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 +// 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) { + // solhint-disable-next-line custom-errors + 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/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; diff --git a/hardhat.config.js b/hardhat.config.js index 232f3920..9d4759ba 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -43,6 +43,7 @@ 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', ], }, zksolc: { diff --git a/package.json b/package.json index a74ad370..c90e8130 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@1inch/limit-order-protocol-contract", - "version": "4.0.3", + "version": "4.1.0", "description": "1inch Limit Order Protocol", "repository": { "type": "git", @@ -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/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'); + }); +}); diff --git a/test/SafeOrderBuilder.js b/test/SafeOrderBuilder.js new file mode 100644 index 00000000..2dd5b230 --- /dev/null +++ b/test/SafeOrderBuilder.js @@ -0,0 +1,119 @@ +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 + // 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(), + 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); + }); + } +}); 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"