Add decoders for broker and stop-limit data (#2484)

* Add decoders for broker and stop-limit data

* update changelogs

* Address comments
This commit is contained in:
mzhu25 2020-02-14 17:38:43 -08:00 committed by GitHub
parent fd47947e55
commit dcce8276b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 244 additions and 132 deletions

View File

@ -1,4 +1,13 @@
[ [
{
"version": "1.1.0",
"changes": [
{
"note": "Added decoders for broker data",
"pr": 2484
}
]
},
{ {
"timestamp": 1581204851, "timestamp": 1581204851,
"version": "1.0.2", "version": "1.0.2",

View File

@ -1,16 +1,26 @@
import { assetDataUtils } from '@0x/order-utils'; import { assetDataUtils } from '@0x/order-utils';
import { ERC1155AssetData } from '@0x/types';
import { AbiEncoder, BigNumber } from '@0x/utils'; import { AbiEncoder, BigNumber } from '@0x/utils';
export const godsUnchainedUtils = { export interface GodsUnchainedProperties {
proto: BigNumber | number;
quality: BigNumber | number;
}
const propertyDataEncoder = AbiEncoder.create([{ name: 'proto', type: 'uint16' }, { name: 'quality', type: 'uint8' }]);
const brokerDataEncoder = AbiEncoder.create([
{ name: 'godsUnchainedAddress', type: 'address' },
{ name: 'validatorAddress', type: 'address' },
{ name: 'propertyData', type: 'bytes' },
]);
/** /**
* Encodes the given proto and quality into the bytes format expected by the GodsUnchainedValidator. * Encodes the given proto and quality into the bytes format expected by the GodsUnchainedValidator.
*/ */
encodePropertyData(proto: BigNumber, quality: BigNumber): string { export function encodePropertyData(properties: GodsUnchainedProperties): string {
return AbiEncoder.create([{ name: 'proto', type: 'uint16' }, { name: 'quality', type: 'uint8' }]).encode({ return propertyDataEncoder.encode(properties);
proto, }
quality,
});
},
/** /**
* Encodes the given proto and quality into ERC1155 asset data to be used as the takerAssetData * Encodes the given proto and quality into ERC1155 asset data to be used as the takerAssetData
* of a property-based GodsUnchained order. Must also provide the addresses of the Broker, * of a property-based GodsUnchained order. Must also provide the addresses of the Broker,
@ -19,24 +29,31 @@ export const godsUnchainedUtils = {
* takerAssetAmount is 3 and the bundleSize is 2, the taker must provide 2, 4, or 6 cards * takerAssetAmount is 3 and the bundleSize is 2, the taker must provide 2, 4, or 6 cards
* with the given proto and quality to fill the order. If an odd number is provided, the fill fails. * with the given proto and quality to fill the order. If an odd number is provided, the fill fails.
*/ */
encodeBrokerAssetData( export function encodeBrokerAssetData(
brokerAddress: string, brokerAddress: string,
godsUnchainedAddress: string, godsUnchainedAddress: string,
validatorAddress: string, validatorAddress: string,
proto: BigNumber, properties: GodsUnchainedProperties,
quality: BigNumber,
bundleSize: number = 1, bundleSize: number = 1,
): string { ): string {
const dataEncoder = AbiEncoder.create([ const propertyData = propertyDataEncoder.encode(properties);
{ name: 'godsUnchainedAddress', type: 'address' }, const brokerData = brokerDataEncoder.encode({ godsUnchainedAddress, validatorAddress, propertyData });
{ name: 'validatorAddress', type: 'address' }, return assetDataUtils.encodeERC1155AssetData(brokerAddress, [], [new BigNumber(bundleSize)], brokerData);
{ name: 'propertyData', type: 'bytes' }, }
]);
const propertyData = AbiEncoder.create([ /**
{ name: 'proto', type: 'uint16' }, * Decodes proto and quality from the bytes format expected by the GodsUnchainedValidator.
{ name: 'quality', type: 'uint8' }, */
]).encode({ proto, quality }); export function decodePropertyData(propertyData: string): GodsUnchainedProperties {
const data = dataEncoder.encode({ godsUnchainedAddress, validatorAddress, propertyData }); return propertyDataEncoder.decode(propertyData);
return assetDataUtils.encodeERC1155AssetData(brokerAddress, [], [new BigNumber(bundleSize)], data); }
},
}; /**
* Decodes proto and quality from the ERC1155 takerAssetData of a property-based GodsUnchained order.
*/
export function decodeBrokerAssetData(brokerAssetData: string): GodsUnchainedProperties {
// tslint:disable-next-line:no-unnecessary-type-assertion
const { callbackData: brokerData } = assetDataUtils.decodeAssetDataOrThrow(brokerAssetData) as ERC1155AssetData;
const { propertyData } = brokerDataEncoder.decode(brokerData);
return decodePropertyData(propertyData);
}

View File

@ -1,6 +1,6 @@
export { artifacts } from './artifacts'; export { artifacts } from './artifacts';
export { BrokerContract, GodsUnchainedValidatorContract, TestGodsUnchainedContract } from './wrappers'; export { BrokerContract, GodsUnchainedValidatorContract, TestGodsUnchainedContract } from './wrappers';
export { godsUnchainedUtils } from './gods_unchained_utils'; export * from './gods_unchained_utils';
export { BrokerRevertErrors } from '@0x/utils'; export { BrokerRevertErrors } from '@0x/utils';
export { export {
ContractArtifact, ContractArtifact,

View File

@ -2,7 +2,7 @@ import { blockchainTests, constants, expect, getRandomInteger } from '@0x/contra
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { godsUnchainedUtils } from '../src/gods_unchained_utils'; import { encodePropertyData } from '../src/gods_unchained_utils';
import { artifacts } from './artifacts'; import { artifacts } from './artifacts';
import { GodsUnchainedValidatorContract, TestGodsUnchainedContract } from './wrappers'; import { GodsUnchainedValidatorContract, TestGodsUnchainedContract } from './wrappers';
@ -33,7 +33,7 @@ blockchainTests.resets('GodsUnchainedValidator unit tests', env => {
describe('checkBrokerAsset', () => { describe('checkBrokerAsset', () => {
const proto = new BigNumber(42); const proto = new BigNumber(42);
const quality = new BigNumber(7); const quality = new BigNumber(7);
const propertyData = godsUnchainedUtils.encodePropertyData(proto, quality); const propertyData = encodePropertyData({ proto, quality });
it('succeeds if assetData proto and quality match propertyData', async () => { it('succeeds if assetData proto and quality match propertyData', async () => {
const tokenId = getRandomInteger(0, constants.MAX_UINT256); const tokenId = getRandomInteger(0, constants.MAX_UINT256);

View File

@ -9,6 +9,10 @@
{ {
"note": "Fixed the mainnet dYdX Bridge tests.", "note": "Fixed the mainnet dYdX Bridge tests.",
"pr": 2479 "pr": 2479
},
{
"note": "Addeded decoders for stop-limit data",
"pr": 2484
} }
] ]
}, },

View File

@ -1,35 +1,58 @@
import { constants } from '@0x/contracts-test-utils'; import { constants } from '@0x/contracts-test-utils';
import { assetDataUtils } from '@0x/order-utils'; import { assetDataUtils } from '@0x/order-utils';
import { StaticCallAssetData } from '@0x/types';
import { AbiEncoder, BigNumber } from '@0x/utils'; import { AbiEncoder, BigNumber } from '@0x/utils';
export interface StopLimitParameters {
oracle: string;
minPrice: BigNumber;
maxPrice: BigNumber;
}
const stopLimitDataEncoder = AbiEncoder.create([
{ name: 'oracle', type: 'address' },
{ name: 'minPrice', type: 'int256' },
{ name: 'maxPrice', type: 'int256' },
]);
const stopLimitMethodEncoder = AbiEncoder.createMethod('checkStopLimit', [{ name: 'stopLimitData', type: 'bytes' }]);
/** /**
* Encodes the given stop limit data parameters into the bytes format expected by the * Encodes the given stop limit data parameters into the bytes format expected by the
* ChainlinkStopLimit contract. * ChainlinkStopLimit contract.
*/ */
export function encodeChainlinkStopLimitData(oracle: string, minPrice: BigNumber, maxPrice: BigNumber): string { export function encodeChainlinkStopLimitData(params: StopLimitParameters): string {
const encoder = AbiEncoder.create([ return stopLimitDataEncoder.encode(params);
{ 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 * Encodes the given stop limit data parameters into StaticCall asset data so that it can be used
* in a 0x order. * in a 0x order.
*/ */
export function encodeStopLimitStaticCallData( export function encodeStopLimitStaticCallData(chainlinkStopLimitAddress: string, params: StopLimitParameters): string {
chainlinkStopLimitAddress: string, const staticCallData = stopLimitMethodEncoder.encode({
oracle: string, stopLimitData: encodeChainlinkStopLimitData(params),
minPrice: BigNumber, });
maxPrice: BigNumber,
): string {
const staticCallData = AbiEncoder.createMethod('checkStopLimit', [{ name: 'stopLimitData', type: 'bytes' }]).encode(
{ stopLimitData: encodeChainlinkStopLimitData(oracle, minPrice, maxPrice) },
);
return assetDataUtils.encodeStaticCallAssetData( return assetDataUtils.encodeStaticCallAssetData(
chainlinkStopLimitAddress, chainlinkStopLimitAddress,
staticCallData, staticCallData,
constants.KECCAK256_NULL, constants.KECCAK256_NULL,
); );
} }
/**
* Decodes stop limit data parameters from the bytes format expected by the ChainlinkStopLimit contract.
*/
export function decodeChainlinkStopLimitData(stopLimitData: string): StopLimitParameters {
return stopLimitDataEncoder.decode(stopLimitData);
}
/**
* Decodes stop limit data parameters from stop limit StaticCall asset data.
*/
export function decodeStopLimitStaticCallData(assetData: string): StopLimitParameters {
// tslint:disable-next-line:no-unnecessary-type-assertion
const { staticCallData } = assetDataUtils.decodeAssetDataOrThrow(assetData) as StaticCallAssetData;
const stopLimitData = stopLimitMethodEncoder.strictDecode<string>(staticCallData);
return decodeChainlinkStopLimitData(stopLimitData);
}

View File

@ -1,14 +1,17 @@
import { import {
artifacts as BrokerArtifacts, artifacts as BrokerArtifacts,
BrokerContract, BrokerContract,
godsUnchainedUtils, decodeBrokerAssetData,
decodePropertyData,
encodeBrokerAssetData,
encodePropertyData,
GodsUnchainedValidatorContract, GodsUnchainedValidatorContract,
TestGodsUnchainedContract, TestGodsUnchainedContract,
} from '@0x/contracts-broker'; } from '@0x/contracts-broker';
import { DummyERC721TokenContract } from '@0x/contracts-erc721'; import { DummyERC721TokenContract } from '@0x/contracts-erc721';
import { ExchangeFunctionName, ExchangeRevertErrors } from '@0x/contracts-exchange'; import { ExchangeFunctionName, ExchangeRevertErrors } from '@0x/contracts-exchange';
import { ReferenceFunctions } from '@0x/contracts-exchange-libs'; import { ReferenceFunctions } from '@0x/contracts-exchange-libs';
import { blockchainTests, constants, expect } from '@0x/contracts-test-utils'; import { blockchainTests, constants, expect, getRandomInteger, randomAddress } from '@0x/contracts-test-utils';
import { assetDataUtils } from '@0x/order-utils'; import { assetDataUtils } from '@0x/order-utils';
import { SignedOrder } from '@0x/types'; import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
@ -72,13 +75,10 @@ blockchainTests.resets('Broker <> Gods Unchained integration tests', env => {
deployment.tokens.weth.address, deployment.tokens.weth.address,
); );
const takerAssetData = godsUnchainedUtils.encodeBrokerAssetData( const takerAssetData = encodeBrokerAssetData(broker.address, godsUnchained.address, validator.address, {
broker.address, proto: makerSpecifiedProto,
godsUnchained.address, quality: makerSpecifiedQuality,
validator.address, });
makerSpecifiedProto,
makerSpecifiedQuality,
);
const orderConfig = { const orderConfig = {
feeRecipientAddress: constants.NULL_ADDRESS, feeRecipientAddress: constants.NULL_ADDRESS,
@ -530,22 +530,16 @@ blockchainTests.resets('Broker <> Gods Unchained integration tests', env => {
orders = [ orders = [
await maker.signOrderAsync({ await maker.signOrderAsync({
takerAssetData: godsUnchainedUtils.encodeBrokerAssetData( takerAssetData: encodeBrokerAssetData(broker.address, godsUnchained.address, validator.address, {
broker.address, proto: firstOrderProto,
godsUnchained.address, quality: firstOrderQuality,
validator.address, }),
firstOrderProto,
firstOrderQuality,
),
}), }),
await maker.signOrderAsync({ await maker.signOrderAsync({
takerAssetData: godsUnchainedUtils.encodeBrokerAssetData( takerAssetData: encodeBrokerAssetData(broker.address, godsUnchained.address, validator.address, {
broker.address, proto: secondOrderProto,
godsUnchained.address, quality: secondOrderQuality,
validator.address, }),
secondOrderProto,
secondOrderQuality,
),
}), }),
]; ];
}); });
@ -718,4 +712,30 @@ blockchainTests.resets('Broker <> Gods Unchained integration tests', env => {
balanceStore.assertEquals(expectedBalances); balanceStore.assertEquals(expectedBalances);
}); });
}); });
describe('Data encoding/decoding tools', () => {
const MAX_UINT8 = 2 ** 8 - 1;
const MAX_UINT16 = 2 ** 16 - 1;
it('correctly decodes property data', async () => {
const properties = {
proto: getRandomInteger(0, MAX_UINT16),
quality: getRandomInteger(0, MAX_UINT8),
};
const encoded = encodePropertyData(properties);
const decoded = decodePropertyData(encoded);
expect(decoded.proto).to.bignumber.equal(properties.proto);
expect(decoded.quality).to.bignumber.equal(properties.quality);
});
it('correctly decodes broker asset data', async () => {
const properties = {
proto: getRandomInteger(0, MAX_UINT16),
quality: getRandomInteger(0, MAX_UINT8),
};
const encoded = encodeBrokerAssetData(randomAddress(), randomAddress(), randomAddress(), properties);
const decoded = decodeBrokerAssetData(encoded);
expect(decoded.proto).to.bignumber.equal(properties.proto);
expect(decoded.quality).to.bignumber.equal(properties.quality);
});
});
}); // tslint:disable-line:max-file-line-count }); // tslint:disable-line:max-file-line-count

View File

@ -1,10 +1,22 @@
import { ExchangeRevertErrors } from '@0x/contracts-exchange'; import { ExchangeRevertErrors } from '@0x/contracts-exchange';
import { blockchainTests, constants, expect, orderHashUtils } from '@0x/contracts-test-utils'; import {
blockchainTests,
constants,
expect,
getRandomInteger,
orderHashUtils,
randomAddress,
} from '@0x/contracts-test-utils';
import { assetDataUtils } from '@0x/order-utils'; import { assetDataUtils } from '@0x/order-utils';
import { SignedOrder } from '@0x/types'; import { SignedOrder } from '@0x/types';
import { BigNumber, StringRevertError } from '@0x/utils'; import { BigNumber, StringRevertError } from '@0x/utils';
import { encodeStopLimitStaticCallData } from '../../src/chainlink_utils'; import {
decodeChainlinkStopLimitData,
decodeStopLimitStaticCallData,
encodeChainlinkStopLimitData,
encodeStopLimitStaticCallData,
} from '../../src/chainlink_utils';
import { artifacts } from '../artifacts'; import { artifacts } from '../artifacts';
import { Actor } from '../framework/actors/base'; import { Actor } from '../framework/actors/base';
@ -21,6 +33,7 @@ blockchainTests.resets('Chainlink stop-limit order tests', env => {
let initialBalances: LocalBalanceStore; let initialBalances: LocalBalanceStore;
let chainLinkAggregator: TestChainlinkAggregatorContract; let chainLinkAggregator: TestChainlinkAggregatorContract;
let chainlinkStopLimit: ChainlinkStopLimitContract;
let maker: Maker; let maker: Maker;
let taker: Taker; let taker: Taker;
@ -38,7 +51,7 @@ blockchainTests.resets('Chainlink stop-limit order tests', env => {
}); });
const [makerToken, takerToken] = deployment.tokens.erc20; const [makerToken, takerToken] = deployment.tokens.erc20;
const chainlinkStopLimit = await ChainlinkStopLimitContract.deployFrom0xArtifactAsync( chainlinkStopLimit = await ChainlinkStopLimitContract.deployFrom0xArtifactAsync(
artifacts.ChainlinkStopLimit, artifacts.ChainlinkStopLimit,
env.provider, env.provider,
env.txDefaults, env.txDefaults,
@ -55,12 +68,11 @@ blockchainTests.resets('Chainlink stop-limit order tests', env => {
[new BigNumber(1), new BigNumber(1)], [new BigNumber(1), new BigNumber(1)],
[ [
assetDataUtils.encodeERC20AssetData(makerToken.address), assetDataUtils.encodeERC20AssetData(makerToken.address),
encodeStopLimitStaticCallData( encodeStopLimitStaticCallData(chainlinkStopLimit.address, {
chainlinkStopLimit.address, oracle: chainLinkAggregator.address,
chainLinkAggregator.address,
minPrice, minPrice,
maxPrice, maxPrice,
), }),
], ],
); );
@ -104,6 +116,7 @@ blockchainTests.resets('Chainlink stop-limit order tests', env => {
Actor.reset(); Actor.reset();
}); });
describe('filling stop-limit orders', () => {
it('fillOrder reverts if price < minPrice', async () => { it('fillOrder reverts if price < minPrice', async () => {
await chainLinkAggregator.setPrice(minPrice.minus(1)).awaitTransactionSuccessAsync(); await chainLinkAggregator.setPrice(minPrice.minus(1)).awaitTransactionSuccessAsync();
const tx = taker.fillOrderAsync(order, order.takerAssetAmount); const tx = taker.fillOrderAsync(order, order.takerAssetAmount);
@ -151,3 +164,29 @@ blockchainTests.resets('Chainlink stop-limit order tests', env => {
balanceStore.assertEquals(expectedBalances); balanceStore.assertEquals(expectedBalances);
}); });
}); });
describe('Data encoding/decoding tools', () => {
const MAX_INT256 = new BigNumber(2).exponentiatedBy(255).minus(1);
it('correctly decodes chainlink stop-limit params', async () => {
const params = {
oracle: randomAddress(),
minPrice: getRandomInteger(0, MAX_INT256),
maxPrice: getRandomInteger(0, MAX_INT256),
};
const encoded = encodeChainlinkStopLimitData(params);
const decoded = decodeChainlinkStopLimitData(encoded);
expect(decoded).to.deep.equal(params);
});
it('correctly decodes stop-limit assetData', async () => {
const params = {
oracle: randomAddress(),
minPrice: getRandomInteger(0, MAX_INT256),
maxPrice: getRandomInteger(0, MAX_INT256),
};
const encoded = encodeStopLimitStaticCallData(chainlinkStopLimit.address, params);
const decoded = decodeStopLimitStaticCallData(encoded);
expect(decoded).to.deep.equal(params);
});
});
});