From f471c79b59ff283ddd97ba1a8a50a1b7b2e2edf9 Mon Sep 17 00:00:00 2001 From: mzhu25 Date: Tue, 11 Feb 2020 15:10:06 -0800 Subject: [PATCH] Chainlink stop-limit orders (#2473) * Contracts for Chainlink stop-limit orders * Tests and asset data utils * Update contracts-integrations changelog * Address comments * Remove priceFreshness parameter * Remove LibSafeMath * fix typo * Add ChainlinkStopLimit addresses to @0x/contract-addresses --- contracts/integrations/CHANGELOG.json | 9 ++ .../contracts/src/ChainlinkStopLimit.sol | 50 ++++++ .../src/interfaces/IChainlinkAggregator.sol | 30 ++++ .../test/TestChainlinkAggregator.sol | 43 +++++ contracts/integrations/package.json | 2 +- contracts/integrations/src/chainlink_utils.ts | 35 ++++ contracts/integrations/src/index.ts | 1 + contracts/integrations/test/artifacts.ts | 6 + .../stop-limit/chainlink_stop_limit_test.ts | 153 ++++++++++++++++++ contracts/integrations/test/wrappers.ts | 3 + contracts/integrations/tsconfig.json | 3 + packages/contract-addresses/CHANGELOG.json | 9 ++ packages/contract-addresses/addresses.json | 15 +- 13 files changed, 353 insertions(+), 6 deletions(-) create mode 100644 contracts/integrations/contracts/src/ChainlinkStopLimit.sol create mode 100644 contracts/integrations/contracts/src/interfaces/IChainlinkAggregator.sol create mode 100644 contracts/integrations/contracts/test/TestChainlinkAggregator.sol create mode 100644 contracts/integrations/src/chainlink_utils.ts create mode 100644 contracts/integrations/test/stop-limit/chainlink_stop_limit_test.ts diff --git a/contracts/integrations/CHANGELOG.json b/contracts/integrations/CHANGELOG.json index 6b6bac3b4c..728bd9feff 100644 --- a/contracts/integrations/CHANGELOG.json +++ b/contracts/integrations/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "2.4.0", + "changes": [ + { + "note": "Added ChainlinkStopLimit contract and tests", + "pr": 2473 + } + ] + }, { "version": "2.3.0", "changes": [ diff --git a/contracts/integrations/contracts/src/ChainlinkStopLimit.sol b/contracts/integrations/contracts/src/ChainlinkStopLimit.sol new file mode 100644 index 0000000000..397e139148 --- /dev/null +++ b/contracts/integrations/contracts/src/ChainlinkStopLimit.sol @@ -0,0 +1,50 @@ +/* + + Copyright 2019 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.5.9; +pragma experimental ABIEncoderV2; + +import "./interfaces/IChainlinkAggregator.sol"; + + +contract ChainlinkStopLimit { + + /// @dev Checks that the price returned by the encoded Chainlink reference contract is + /// within the encoded price range. + /// @param stopLimitData Encodes the address of the Chainlink reference contract and the + /// valid price range. + function checkStopLimit(bytes calldata stopLimitData) + external + view + { + ( + address oracle, + int256 minPrice, + int256 maxPrice + ) = abi.decode( + stopLimitData, + (address, int256, int256) + ); + + int256 latestPrice = IChainlinkAggregator(oracle).latestAnswer(); + require( + latestPrice >= minPrice && latestPrice <= maxPrice, + "ChainlinkStopLimit/OUT_OF_PRICE_RANGE" + ); + } +} diff --git a/contracts/integrations/contracts/src/interfaces/IChainlinkAggregator.sol b/contracts/integrations/contracts/src/interfaces/IChainlinkAggregator.sol new file mode 100644 index 0000000000..08cf6af408 --- /dev/null +++ b/contracts/integrations/contracts/src/interfaces/IChainlinkAggregator.sol @@ -0,0 +1,30 @@ +/* + + Copyright 2019 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.5.9; +pragma experimental ABIEncoderV2; + + +// A subset of https://github.com/smartcontractkit/chainlink/blob/master/evm/contracts/interfaces/AggregatorInterface.sol +interface IChainlinkAggregator { + + /// @dev Returns the latest data value recorded by the contract. + /// @return answer The latest data value recorded. For a price oracle aggregator, this will be + /// the price of the given asset in USD, multipled by 10^8 + function latestAnswer() external view returns (int256 answer); +} diff --git a/contracts/integrations/contracts/test/TestChainlinkAggregator.sol b/contracts/integrations/contracts/test/TestChainlinkAggregator.sol new file mode 100644 index 0000000000..4626e20dad --- /dev/null +++ b/contracts/integrations/contracts/test/TestChainlinkAggregator.sol @@ -0,0 +1,43 @@ +/* + + Copyright 2019 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.5.9; +pragma experimental ABIEncoderV2; + +import "../src/interfaces/IChainlinkAggregator.sol"; + + +contract TestChainlinkAggregator is + IChainlinkAggregator +{ + int256 internal _price; + + function setPrice(int256 price_) + external + { + _price = price_; + } + + function latestAnswer() + external + view + returns (int256) + { + return _price; + } +} diff --git a/contracts/integrations/package.json b/contracts/integrations/package.json index afd8a90337..681ee4ee0b 100644 --- a/contracts/integrations/package.json +++ b/contracts/integrations/package.json @@ -38,7 +38,7 @@ }, "config": { "publicInterfaceContracts": "TestFramework", - "abis": "./test/generated-artifacts/@(TestContractWrapper|TestDydxUser|TestEth2Dai|TestEth2DaiBridge|TestFramework|TestMainnetAggregatorFills|TestSignatureValidationWallet|TestUniswapBridge|TestUniswapExchange|TestUniswapExchangeFactory).json", + "abis": "./test/generated-artifacts/@(ChainlinkStopLimit|IChainlinkAggregator|TestChainlinkAggregator|TestContractWrapper|TestDydxUser|TestEth2Dai|TestEth2DaiBridge|TestFramework|TestMainnetAggregatorFills|TestSignatureValidationWallet|TestUniswapBridge|TestUniswapExchange|TestUniswapExchangeFactory).json", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually." }, "repository": { diff --git a/contracts/integrations/src/chainlink_utils.ts b/contracts/integrations/src/chainlink_utils.ts new file mode 100644 index 0000000000..7406aa20ba --- /dev/null +++ b/contracts/integrations/src/chainlink_utils.ts @@ -0,0 +1,35 @@ +import { constants } from '@0x/contracts-test-utils'; +import { assetDataUtils } from '@0x/order-utils'; +import { AbiEncoder, BigNumber } from '@0x/utils'; + +/** + * Encodes the given stop limit data parameters into the bytes format expected by the + * ChainlinkStopLimit contract. + */ +export function encodeChainlinkStopLimitData(oracle: string, minPrice: BigNumber, maxPrice: BigNumber): string { + const encoder = AbiEncoder.create([ + { name: 'oracle', type: 'address' }, + { name: 'minPrice', type: 'int256' }, + { name: 'maxPrice', type: 'int256' }, + ]); + return encoder.encode({ oracle, minPrice, maxPrice }); +} +/** + * Encodes the given stop limit data parameters into StaticCall asset data so that it can be used + * in a 0x order. + */ +export function encodeStopLimitStaticCallData( + chainlinkStopLimitAddress: string, + oracle: string, + minPrice: BigNumber, + maxPrice: BigNumber, +): string { + const staticCallData = AbiEncoder.createMethod('checkStopLimit', [{ name: 'stopLimitData', type: 'bytes' }]).encode( + { stopLimitData: encodeChainlinkStopLimitData(oracle, minPrice, maxPrice) }, + ); + return assetDataUtils.encodeStaticCallAssetData( + chainlinkStopLimitAddress, + staticCallData, + constants.KECCAK256_NULL, + ); +} diff --git a/contracts/integrations/src/index.ts b/contracts/integrations/src/index.ts index cb85aa0d18..e25fb51cdb 100644 --- a/contracts/integrations/src/index.ts +++ b/contracts/integrations/src/index.ts @@ -1,2 +1,3 @@ export { artifacts } from './artifacts'; export * from './wrappers'; +export * from './chainlink_utils'; diff --git a/contracts/integrations/test/artifacts.ts b/contracts/integrations/test/artifacts.ts index fe3112296c..e15210daa0 100644 --- a/contracts/integrations/test/artifacts.ts +++ b/contracts/integrations/test/artifacts.ts @@ -5,6 +5,9 @@ */ import { ContractArtifact } from 'ethereum-types'; +import * as ChainlinkStopLimit from '../test/generated-artifacts/ChainlinkStopLimit.json'; +import * as IChainlinkAggregator from '../test/generated-artifacts/IChainlinkAggregator.json'; +import * as TestChainlinkAggregator from '../test/generated-artifacts/TestChainlinkAggregator.json'; import * as TestContractWrapper from '../test/generated-artifacts/TestContractWrapper.json'; import * as TestDydxUser from '../test/generated-artifacts/TestDydxUser.json'; import * as TestEth2Dai from '../test/generated-artifacts/TestEth2Dai.json'; @@ -16,6 +19,9 @@ import * as TestUniswapBridge from '../test/generated-artifacts/TestUniswapBridg import * as TestUniswapExchange from '../test/generated-artifacts/TestUniswapExchange.json'; import * as TestUniswapExchangeFactory from '../test/generated-artifacts/TestUniswapExchangeFactory.json'; export const artifacts = { + ChainlinkStopLimit: ChainlinkStopLimit as ContractArtifact, + IChainlinkAggregator: IChainlinkAggregator as ContractArtifact, + TestChainlinkAggregator: TestChainlinkAggregator as ContractArtifact, TestContractWrapper: TestContractWrapper as ContractArtifact, TestDydxUser: TestDydxUser as ContractArtifact, TestEth2Dai: TestEth2Dai as ContractArtifact, diff --git a/contracts/integrations/test/stop-limit/chainlink_stop_limit_test.ts b/contracts/integrations/test/stop-limit/chainlink_stop_limit_test.ts new file mode 100644 index 0000000000..cb77fd9084 --- /dev/null +++ b/contracts/integrations/test/stop-limit/chainlink_stop_limit_test.ts @@ -0,0 +1,153 @@ +import { ExchangeRevertErrors } from '@0x/contracts-exchange'; +import { blockchainTests, constants, expect, orderHashUtils } from '@0x/contracts-test-utils'; +import { assetDataUtils } from '@0x/order-utils'; +import { SignedOrder } from '@0x/types'; +import { BigNumber, StringRevertError } from '@0x/utils'; + +import { encodeStopLimitStaticCallData } from '../../src/chainlink_utils'; + +import { artifacts } from '../artifacts'; +import { Actor } from '../framework/actors/base'; +import { Maker } from '../framework/actors/maker'; +import { Taker } from '../framework/actors/taker'; +import { BlockchainBalanceStore } from '../framework/balances/blockchain_balance_store'; +import { LocalBalanceStore } from '../framework/balances/local_balance_store'; +import { DeploymentManager } from '../framework/deployment_manager'; +import { ChainlinkStopLimitContract, TestChainlinkAggregatorContract } from '../wrappers'; + +blockchainTests.resets('Chainlink stop-limit order tests', env => { + let deployment: DeploymentManager; + let balanceStore: BlockchainBalanceStore; + let initialBalances: LocalBalanceStore; + + let chainLinkAggregator: TestChainlinkAggregatorContract; + + let maker: Maker; + let taker: Taker; + + let order: SignedOrder; + + const minPrice = new BigNumber(42); + const maxPrice = new BigNumber(1337); + + before(async () => { + deployment = await DeploymentManager.deployAsync(env, { + numErc20TokensToDeploy: 2, + numErc721TokensToDeploy: 0, + numErc1155TokensToDeploy: 0, + }); + const [makerToken, takerToken] = deployment.tokens.erc20; + + const chainlinkStopLimit = await ChainlinkStopLimitContract.deployFrom0xArtifactAsync( + artifacts.ChainlinkStopLimit, + env.provider, + env.txDefaults, + artifacts, + ); + chainLinkAggregator = await TestChainlinkAggregatorContract.deployFrom0xArtifactAsync( + artifacts.TestChainlinkAggregator, + env.provider, + env.txDefaults, + artifacts, + ); + + const makerAssetData = assetDataUtils.encodeMultiAssetData( + [new BigNumber(1), new BigNumber(1)], + [ + assetDataUtils.encodeERC20AssetData(makerToken.address), + encodeStopLimitStaticCallData( + chainlinkStopLimit.address, + chainLinkAggregator.address, + minPrice, + maxPrice, + ), + ], + ); + + const orderConfig = { + feeRecipientAddress: constants.NULL_ADDRESS, + makerAssetData, + takerAssetData: assetDataUtils.encodeERC20AssetData(takerToken.address), + makerFeeAssetData: constants.NULL_BYTES, + takerFeeAssetData: constants.NULL_BYTES, + makerFee: constants.ZERO_AMOUNT, + takerFee: constants.ZERO_AMOUNT, + }; + + maker = new Maker({ + name: 'Maker', + deployment, + orderConfig, + }); + taker = new Taker({ name: 'Taker', deployment }); + + // Set balances and allowances + await maker.configureERC20TokenAsync(makerToken); + await taker.configureERC20TokenAsync(takerToken); + + order = await maker.signOrderAsync(); + + // Set up balance stores + const tokenOwners = { + Maker: maker.address, + Taker: taker.address, + }; + const tokenContracts = { + erc20: { makerToken, takerToken }, + }; + balanceStore = new BlockchainBalanceStore(tokenOwners, tokenContracts); + await balanceStore.updateBalancesAsync(); + initialBalances = LocalBalanceStore.create(balanceStore); + }); + + after(async () => { + Actor.reset(); + }); + + it('fillOrder reverts if price < minPrice', async () => { + await chainLinkAggregator.setPrice(minPrice.minus(1)).awaitTransactionSuccessAsync(); + const tx = taker.fillOrderAsync(order, order.takerAssetAmount); + const expectedError = new ExchangeRevertErrors.AssetProxyTransferError( + orderHashUtils.getOrderHashHex(order), + order.makerAssetData, + new StringRevertError('ChainlinkStopLimit/OUT_OF_PRICE_RANGE').encode(), + ); + return expect(tx).to.revertWith(expectedError); + }); + it('fillOrder reverts price > maxPrice', async () => { + await chainLinkAggregator.setPrice(maxPrice.plus(1)).awaitTransactionSuccessAsync(); + const tx = taker.fillOrderAsync(order, order.takerAssetAmount); + const expectedError = new ExchangeRevertErrors.AssetProxyTransferError( + orderHashUtils.getOrderHashHex(order), + order.makerAssetData, + new StringRevertError('ChainlinkStopLimit/OUT_OF_PRICE_RANGE').encode(), + ); + return expect(tx).to.revertWith(expectedError); + }); + it('fillOrder succeeds if price = minPrice', async () => { + await chainLinkAggregator.setPrice(minPrice).awaitTransactionSuccessAsync(); + const receipt = await taker.fillOrderAsync(order, order.takerAssetAmount); + const expectedBalances = LocalBalanceStore.create(initialBalances); + expectedBalances.simulateFills([order], taker.address, receipt, deployment, DeploymentManager.protocolFee); + await balanceStore.updateBalancesAsync(); + balanceStore.assertEquals(expectedBalances); + }); + it('fillOrder succeeds if price = maxPrice', async () => { + await chainLinkAggregator.setPrice(maxPrice).awaitTransactionSuccessAsync(); + const receipt = await taker.fillOrderAsync(order, order.takerAssetAmount); + const expectedBalances = LocalBalanceStore.create(initialBalances); + expectedBalances.simulateFills([order], taker.address, receipt, deployment, DeploymentManager.protocolFee); + await balanceStore.updateBalancesAsync(); + balanceStore.assertEquals(expectedBalances); + }); + it('fillOrder succeeds if minPrice < price < maxPrice', async () => { + await chainLinkAggregator + .setPrice(minPrice.plus(maxPrice).dividedToIntegerBy(2)) + .awaitTransactionSuccessAsync(); + const receipt = await taker.fillOrderAsync(order, order.takerAssetAmount); + const expectedBalances = LocalBalanceStore.create(initialBalances); + expectedBalances.simulateFills([order], taker.address, receipt, deployment, DeploymentManager.protocolFee); + await balanceStore.updateBalancesAsync(); + balanceStore.assertEquals(expectedBalances); + }); +}); diff --git a/contracts/integrations/test/wrappers.ts b/contracts/integrations/test/wrappers.ts index d9a1dad83d..853d6dd18a 100644 --- a/contracts/integrations/test/wrappers.ts +++ b/contracts/integrations/test/wrappers.ts @@ -3,6 +3,9 @@ * Warning: This file is auto-generated by contracts-gen. Don't edit manually. * ----------------------------------------------------------------------------- */ +export * from '../test/generated-wrappers/chainlink_stop_limit'; +export * from '../test/generated-wrappers/i_chainlink_aggregator'; +export * from '../test/generated-wrappers/test_chainlink_aggregator'; export * from '../test/generated-wrappers/test_contract_wrapper'; export * from '../test/generated-wrappers/test_dydx_user'; export * from '../test/generated-wrappers/test_eth2_dai'; diff --git a/contracts/integrations/tsconfig.json b/contracts/integrations/tsconfig.json index 081fefff5e..7e86ada82d 100644 --- a/contracts/integrations/tsconfig.json +++ b/contracts/integrations/tsconfig.json @@ -4,6 +4,9 @@ "include": ["./src/**/*", "./test/**/*", "./generated-wrappers/**/*"], "files": [ "generated-artifacts/TestFramework.json", + "test/generated-artifacts/ChainlinkStopLimit.json", + "test/generated-artifacts/IChainlinkAggregator.json", + "test/generated-artifacts/TestChainlinkAggregator.json", "test/generated-artifacts/TestContractWrapper.json", "test/generated-artifacts/TestDydxUser.json", "test/generated-artifacts/TestEth2Dai.json", diff --git a/packages/contract-addresses/CHANGELOG.json b/packages/contract-addresses/CHANGELOG.json index 83119a6a74..f8d7c5b6df 100644 --- a/packages/contract-addresses/CHANGELOG.json +++ b/packages/contract-addresses/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "4.6.0", + "changes": [ + { + "note": "Added ChainlinkStopLimit addresses (mainnet, ropsten, rinkeby)", + "pr": 2473 + } + ] + }, { "version": "4.5.0", "changes": [ diff --git a/packages/contract-addresses/addresses.json b/packages/contract-addresses/addresses.json index 22fc07ae4a..6ba45911a7 100644 --- a/packages/contract-addresses/addresses.json +++ b/packages/contract-addresses/addresses.json @@ -26,7 +26,8 @@ "chaiBridge": "0x77c31eba23043b9a72d13470f3a3a311344d7438", "dydxBridge": "0x55dc8f21d20d4c6ed3c82916a438a413ca68e335", "godsUnchainedValidator": "0x09A379Ef7218BCFD8913fAa8B281ebc5A2E0bC04", - "broker": "0xd4690a51044db77D91d7Aa8f7a3a5ad5dA331Af0" + "broker": "0xd4690a51044db77D91d7Aa8f7a3a5ad5dA331Af0", + "chainlinkStopLimit": "0xeb27220f95f364e1d9531992c48613f231839f53" }, "3": { "erc20Proxy": "0xb1408f4c245a23c31b98d2c626777d4c0d766caa", @@ -55,7 +56,8 @@ "chaiBridge": "0x0000000000000000000000000000000000000000", "dydxBridge": "0x0000000000000000000000000000000000000000", "godsUnchainedValidator": "0xd4690a51044db77D91d7Aa8f7a3a5ad5dA331Af0", - "broker": "0x4Aa817C6f383C8e8aE77301d18Ce48efb16Fd2BE" + "broker": "0x4Aa817C6f383C8e8aE77301d18Ce48efb16Fd2BE", + "chainlinkStopLimit": "0x67a094cf028221ffdd93fc658f963151d05e2a74" }, "4": { "exchangeV2": "0xbff9493f92a3df4b0429b6d00743b3cfb4c85831", @@ -84,7 +86,8 @@ "chaiBridge": "0x0000000000000000000000000000000000000000", "dydxBridge": "0x0000000000000000000000000000000000000000", "godsUnchainedValidator": "0x0000000000000000000000000000000000000000", - "broker": "0x0000000000000000000000000000000000000000" + "broker": "0x0000000000000000000000000000000000000000", + "chainlinkStopLimit": "0x407b4128e9ecad8769b2332312a9f655cb9f5f3a" }, "42": { "erc20Proxy": "0xf1ec01d6236d3cd881a0bf0130ea25fe4234003e", @@ -113,7 +116,8 @@ "chaiBridge": "0x0000000000000000000000000000000000000000", "dydxBridge": "0x0000000000000000000000000000000000000000", "godsUnchainedValidator": "0x0000000000000000000000000000000000000000", - "broker": "0x0000000000000000000000000000000000000000" + "broker": "0x0000000000000000000000000000000000000000", + "chainlinkStopLimit": "0x0000000000000000000000000000000000000000" }, "1337": { "erc20Proxy": "0x1dc4c1cefef38a777b15aa20260a54e584b16c48", @@ -142,6 +146,7 @@ "chaiBridge": "0x0000000000000000000000000000000000000000", "dydxBridge": "0x0000000000000000000000000000000000000000", "godsUnchainedValidator": "0x0000000000000000000000000000000000000000", - "broker": "0x0000000000000000000000000000000000000000" + "broker": "0x0000000000000000000000000000000000000000", + "chainlinkStopLimit": "0x0000000000000000000000000000000000000000" } }