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
This commit is contained in:
mzhu25 2020-02-11 15:10:06 -08:00 committed by GitHub
parent dfd9443f74
commit f471c79b59
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 353 additions and 6 deletions

View File

@ -1,4 +1,13 @@
[ [
{
"version": "2.4.0",
"changes": [
{
"note": "Added ChainlinkStopLimit contract and tests",
"pr": 2473
}
]
},
{ {
"version": "2.3.0", "version": "2.3.0",
"changes": [ "changes": [

View File

@ -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"
);
}
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -38,7 +38,7 @@
}, },
"config": { "config": {
"publicInterfaceContracts": "TestFramework", "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." "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually."
}, },
"repository": { "repository": {

View File

@ -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,
);
}

View File

@ -1,2 +1,3 @@
export { artifacts } from './artifacts'; export { artifacts } from './artifacts';
export * from './wrappers'; export * from './wrappers';
export * from './chainlink_utils';

View File

@ -5,6 +5,9 @@
*/ */
import { ContractArtifact } from 'ethereum-types'; 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 TestContractWrapper from '../test/generated-artifacts/TestContractWrapper.json';
import * as TestDydxUser from '../test/generated-artifacts/TestDydxUser.json'; import * as TestDydxUser from '../test/generated-artifacts/TestDydxUser.json';
import * as TestEth2Dai from '../test/generated-artifacts/TestEth2Dai.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 TestUniswapExchange from '../test/generated-artifacts/TestUniswapExchange.json';
import * as TestUniswapExchangeFactory from '../test/generated-artifacts/TestUniswapExchangeFactory.json'; import * as TestUniswapExchangeFactory from '../test/generated-artifacts/TestUniswapExchangeFactory.json';
export const artifacts = { export const artifacts = {
ChainlinkStopLimit: ChainlinkStopLimit as ContractArtifact,
IChainlinkAggregator: IChainlinkAggregator as ContractArtifact,
TestChainlinkAggregator: TestChainlinkAggregator as ContractArtifact,
TestContractWrapper: TestContractWrapper as ContractArtifact, TestContractWrapper: TestContractWrapper as ContractArtifact,
TestDydxUser: TestDydxUser as ContractArtifact, TestDydxUser: TestDydxUser as ContractArtifact,
TestEth2Dai: TestEth2Dai as ContractArtifact, TestEth2Dai: TestEth2Dai as ContractArtifact,

View File

@ -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);
});
});

View File

@ -3,6 +3,9 @@
* Warning: This file is auto-generated by contracts-gen. Don't edit manually. * 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_contract_wrapper';
export * from '../test/generated-wrappers/test_dydx_user'; export * from '../test/generated-wrappers/test_dydx_user';
export * from '../test/generated-wrappers/test_eth2_dai'; export * from '../test/generated-wrappers/test_eth2_dai';

View File

@ -4,6 +4,9 @@
"include": ["./src/**/*", "./test/**/*", "./generated-wrappers/**/*"], "include": ["./src/**/*", "./test/**/*", "./generated-wrappers/**/*"],
"files": [ "files": [
"generated-artifacts/TestFramework.json", "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/TestContractWrapper.json",
"test/generated-artifacts/TestDydxUser.json", "test/generated-artifacts/TestDydxUser.json",
"test/generated-artifacts/TestEth2Dai.json", "test/generated-artifacts/TestEth2Dai.json",

View File

@ -1,4 +1,13 @@
[ [
{
"version": "4.6.0",
"changes": [
{
"note": "Added ChainlinkStopLimit addresses (mainnet, ropsten, rinkeby)",
"pr": 2473
}
]
},
{ {
"version": "4.5.0", "version": "4.5.0",
"changes": [ "changes": [

View File

@ -26,7 +26,8 @@
"chaiBridge": "0x77c31eba23043b9a72d13470f3a3a311344d7438", "chaiBridge": "0x77c31eba23043b9a72d13470f3a3a311344d7438",
"dydxBridge": "0x55dc8f21d20d4c6ed3c82916a438a413ca68e335", "dydxBridge": "0x55dc8f21d20d4c6ed3c82916a438a413ca68e335",
"godsUnchainedValidator": "0x09A379Ef7218BCFD8913fAa8B281ebc5A2E0bC04", "godsUnchainedValidator": "0x09A379Ef7218BCFD8913fAa8B281ebc5A2E0bC04",
"broker": "0xd4690a51044db77D91d7Aa8f7a3a5ad5dA331Af0" "broker": "0xd4690a51044db77D91d7Aa8f7a3a5ad5dA331Af0",
"chainlinkStopLimit": "0xeb27220f95f364e1d9531992c48613f231839f53"
}, },
"3": { "3": {
"erc20Proxy": "0xb1408f4c245a23c31b98d2c626777d4c0d766caa", "erc20Proxy": "0xb1408f4c245a23c31b98d2c626777d4c0d766caa",
@ -55,7 +56,8 @@
"chaiBridge": "0x0000000000000000000000000000000000000000", "chaiBridge": "0x0000000000000000000000000000000000000000",
"dydxBridge": "0x0000000000000000000000000000000000000000", "dydxBridge": "0x0000000000000000000000000000000000000000",
"godsUnchainedValidator": "0xd4690a51044db77D91d7Aa8f7a3a5ad5dA331Af0", "godsUnchainedValidator": "0xd4690a51044db77D91d7Aa8f7a3a5ad5dA331Af0",
"broker": "0x4Aa817C6f383C8e8aE77301d18Ce48efb16Fd2BE" "broker": "0x4Aa817C6f383C8e8aE77301d18Ce48efb16Fd2BE",
"chainlinkStopLimit": "0x67a094cf028221ffdd93fc658f963151d05e2a74"
}, },
"4": { "4": {
"exchangeV2": "0xbff9493f92a3df4b0429b6d00743b3cfb4c85831", "exchangeV2": "0xbff9493f92a3df4b0429b6d00743b3cfb4c85831",
@ -84,7 +86,8 @@
"chaiBridge": "0x0000000000000000000000000000000000000000", "chaiBridge": "0x0000000000000000000000000000000000000000",
"dydxBridge": "0x0000000000000000000000000000000000000000", "dydxBridge": "0x0000000000000000000000000000000000000000",
"godsUnchainedValidator": "0x0000000000000000000000000000000000000000", "godsUnchainedValidator": "0x0000000000000000000000000000000000000000",
"broker": "0x0000000000000000000000000000000000000000" "broker": "0x0000000000000000000000000000000000000000",
"chainlinkStopLimit": "0x407b4128e9ecad8769b2332312a9f655cb9f5f3a"
}, },
"42": { "42": {
"erc20Proxy": "0xf1ec01d6236d3cd881a0bf0130ea25fe4234003e", "erc20Proxy": "0xf1ec01d6236d3cd881a0bf0130ea25fe4234003e",
@ -113,7 +116,8 @@
"chaiBridge": "0x0000000000000000000000000000000000000000", "chaiBridge": "0x0000000000000000000000000000000000000000",
"dydxBridge": "0x0000000000000000000000000000000000000000", "dydxBridge": "0x0000000000000000000000000000000000000000",
"godsUnchainedValidator": "0x0000000000000000000000000000000000000000", "godsUnchainedValidator": "0x0000000000000000000000000000000000000000",
"broker": "0x0000000000000000000000000000000000000000" "broker": "0x0000000000000000000000000000000000000000",
"chainlinkStopLimit": "0x0000000000000000000000000000000000000000"
}, },
"1337": { "1337": {
"erc20Proxy": "0x1dc4c1cefef38a777b15aa20260a54e584b16c48", "erc20Proxy": "0x1dc4c1cefef38a777b15aa20260a54e584b16c48",
@ -142,6 +146,7 @@
"chaiBridge": "0x0000000000000000000000000000000000000000", "chaiBridge": "0x0000000000000000000000000000000000000000",
"dydxBridge": "0x0000000000000000000000000000000000000000", "dydxBridge": "0x0000000000000000000000000000000000000000",
"godsUnchainedValidator": "0x0000000000000000000000000000000000000000", "godsUnchainedValidator": "0x0000000000000000000000000000000000000000",
"broker": "0x0000000000000000000000000000000000000000" "broker": "0x0000000000000000000000000000000000000000",
"chainlinkStopLimit": "0x0000000000000000000000000000000000000000"
} }
} }