Skip to content

Commit

Permalink
Merge pull request #325 from 1inch/feature/order-registrator
Browse files Browse the repository at this point in the history
[SC-1186 SC-1188] create OrderRegistrator and SafeOrderBuilder
  • Loading branch information
ZumZoom authored Jun 28, 2024
2 parents e28fc67 + e8aeaa0 commit 91ad5df
Show file tree
Hide file tree
Showing 10 changed files with 573 additions and 1 deletion.
45 changes: 45 additions & 0 deletions contracts/helpers/OrderRegistrator.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
83 changes: 83 additions & 0 deletions contracts/helpers/SafeOrderBuilder.sol
Original file line number Diff line number Diff line change
@@ -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));
}
}
28 changes: 28 additions & 0 deletions contracts/interfaces/IOrderRegistrator.sol
Original file line number Diff line number Diff line change
@@ -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;
}
149 changes: 149 additions & 0 deletions contracts/mocks/CompatibilityFallbackHandler.sol
Original file line number Diff line number Diff line change
@@ -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 - <[email protected]>
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))
}
}
}
}
65 changes: 65 additions & 0 deletions deploy/deploy-SafeOrderBuilder.js
Original file line number Diff line number Diff line change
@@ -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;
1 change: 1 addition & 0 deletions hardhat.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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": {
Expand Down
Loading

0 comments on commit 91ad5df

Please sign in to comment.