-
Notifications
You must be signed in to change notification settings - Fork 102
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #325 from 1inch/feature/order-registrator
[SC-1186 SC-1188] create OrderRegistrator and SafeOrderBuilder
- Loading branch information
Showing
10 changed files
with
573 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.