diff --git a/contracts/broker/CHANGELOG.json b/contracts/broker/CHANGELOG.json index 2f8677bc91..ac16910c59 100644 --- a/contracts/broker/CHANGELOG.json +++ b/contracts/broker/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "1.1.0", + "changes": [ + { + "note": "Added decoders for broker data", + "pr": 2484 + } + ] + }, { "timestamp": 1581204851, "version": "1.0.2", diff --git a/contracts/broker/src/gods_unchained_utils.ts b/contracts/broker/src/gods_unchained_utils.ts index b1e2494954..55a5ab3d4b 100644 --- a/contracts/broker/src/gods_unchained_utils.ts +++ b/contracts/broker/src/gods_unchained_utils.ts @@ -1,42 +1,59 @@ import { assetDataUtils } from '@0x/order-utils'; +import { ERC1155AssetData } from '@0x/types'; import { AbiEncoder, BigNumber } from '@0x/utils'; -export const godsUnchainedUtils = { - /** - * Encodes the given proto and quality into the bytes format expected by the GodsUnchainedValidator. - */ - encodePropertyData(proto: BigNumber, quality: BigNumber): string { - return AbiEncoder.create([{ name: 'proto', type: 'uint16' }, { name: 'quality', type: 'uint8' }]).encode({ - proto, - quality, - }); - }, - /** - * 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, - * GodsUnchained, and GodsUnchainedValidator contracts. The optional bundleSize parameter specifies - * how many cards are expected for each "unit" of the takerAssetAmount. For example, If the - * 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. - */ - encodeBrokerAssetData( - brokerAddress: string, - godsUnchainedAddress: string, - validatorAddress: string, - proto: BigNumber, - quality: BigNumber, - bundleSize: number = 1, - ): string { - const dataEncoder = AbiEncoder.create([ - { name: 'godsUnchainedAddress', type: 'address' }, - { name: 'validatorAddress', type: 'address' }, - { name: 'propertyData', type: 'bytes' }, - ]); - const propertyData = AbiEncoder.create([ - { name: 'proto', type: 'uint16' }, - { name: 'quality', type: 'uint8' }, - ]).encode({ proto, quality }); - const data = dataEncoder.encode({ godsUnchainedAddress, validatorAddress, propertyData }); - return assetDataUtils.encodeERC1155AssetData(brokerAddress, [], [new BigNumber(bundleSize)], data); - }, -}; +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. + */ +export function encodePropertyData(properties: GodsUnchainedProperties): string { + return propertyDataEncoder.encode(properties); +} + +/** + * 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, + * GodsUnchained, and GodsUnchainedValidator contracts. The optional bundleSize parameter specifies + * how many cards are expected for each "unit" of the takerAssetAmount. For example, If the + * 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. + */ +export function encodeBrokerAssetData( + brokerAddress: string, + godsUnchainedAddress: string, + validatorAddress: string, + properties: GodsUnchainedProperties, + bundleSize: number = 1, +): string { + const propertyData = propertyDataEncoder.encode(properties); + const brokerData = brokerDataEncoder.encode({ godsUnchainedAddress, validatorAddress, propertyData }); + return assetDataUtils.encodeERC1155AssetData(brokerAddress, [], [new BigNumber(bundleSize)], brokerData); +} + +/** + * Decodes proto and quality from the bytes format expected by the GodsUnchainedValidator. + */ +export function decodePropertyData(propertyData: string): GodsUnchainedProperties { + return propertyDataEncoder.decode(propertyData); +} + +/** + * 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); +} diff --git a/contracts/broker/src/index.ts b/contracts/broker/src/index.ts index 74006467cc..1f13b98d10 100644 --- a/contracts/broker/src/index.ts +++ b/contracts/broker/src/index.ts @@ -1,6 +1,6 @@ export { artifacts } from './artifacts'; export { BrokerContract, GodsUnchainedValidatorContract, TestGodsUnchainedContract } from './wrappers'; -export { godsUnchainedUtils } from './gods_unchained_utils'; +export * from './gods_unchained_utils'; export { BrokerRevertErrors } from '@0x/utils'; export { ContractArtifact, diff --git a/contracts/broker/test/gods_unchained_validator_test.ts b/contracts/broker/test/gods_unchained_validator_test.ts index e19e0a9986..1e17f9bbad 100644 --- a/contracts/broker/test/gods_unchained_validator_test.ts +++ b/contracts/broker/test/gods_unchained_validator_test.ts @@ -2,7 +2,7 @@ import { blockchainTests, constants, expect, getRandomInteger } from '@0x/contra import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; -import { godsUnchainedUtils } from '../src/gods_unchained_utils'; +import { encodePropertyData } from '../src/gods_unchained_utils'; import { artifacts } from './artifacts'; import { GodsUnchainedValidatorContract, TestGodsUnchainedContract } from './wrappers'; @@ -33,7 +33,7 @@ blockchainTests.resets('GodsUnchainedValidator unit tests', env => { describe('checkBrokerAsset', () => { const proto = new BigNumber(42); 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 () => { const tokenId = getRandomInteger(0, constants.MAX_UINT256); diff --git a/contracts/integrations/CHANGELOG.json b/contracts/integrations/CHANGELOG.json index 4c7cba3f25..d330e3a3b6 100644 --- a/contracts/integrations/CHANGELOG.json +++ b/contracts/integrations/CHANGELOG.json @@ -9,6 +9,10 @@ { "note": "Fixed the mainnet dYdX Bridge tests.", "pr": 2479 + }, + { + "note": "Addeded decoders for stop-limit data", + "pr": 2484 } ] }, diff --git a/contracts/integrations/src/chainlink_utils.ts b/contracts/integrations/src/chainlink_utils.ts index 7406aa20ba..9e45621f32 100644 --- a/contracts/integrations/src/chainlink_utils.ts +++ b/contracts/integrations/src/chainlink_utils.ts @@ -1,35 +1,58 @@ import { constants } from '@0x/contracts-test-utils'; import { assetDataUtils } from '@0x/order-utils'; +import { StaticCallAssetData } from '@0x/types'; 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 * 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 }); +export function encodeChainlinkStopLimitData(params: StopLimitParameters): string { + return stopLimitDataEncoder.encode(params); } + /** * 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) }, - ); +export function encodeStopLimitStaticCallData(chainlinkStopLimitAddress: string, params: StopLimitParameters): string { + const staticCallData = stopLimitMethodEncoder.encode({ + stopLimitData: encodeChainlinkStopLimitData(params), + }); return assetDataUtils.encodeStaticCallAssetData( chainlinkStopLimitAddress, staticCallData, 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(staticCallData); + return decodeChainlinkStopLimitData(stopLimitData); +} diff --git a/contracts/integrations/test/broker/broker_test.ts b/contracts/integrations/test/broker/broker_test.ts index 8dd4256221..7252c12f1d 100644 --- a/contracts/integrations/test/broker/broker_test.ts +++ b/contracts/integrations/test/broker/broker_test.ts @@ -1,14 +1,17 @@ import { artifacts as BrokerArtifacts, BrokerContract, - godsUnchainedUtils, + decodeBrokerAssetData, + decodePropertyData, + encodeBrokerAssetData, + encodePropertyData, GodsUnchainedValidatorContract, TestGodsUnchainedContract, } from '@0x/contracts-broker'; import { DummyERC721TokenContract } from '@0x/contracts-erc721'; import { ExchangeFunctionName, ExchangeRevertErrors } from '@0x/contracts-exchange'; 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 { SignedOrder } from '@0x/types'; import { BigNumber } from '@0x/utils'; @@ -72,13 +75,10 @@ blockchainTests.resets('Broker <> Gods Unchained integration tests', env => { deployment.tokens.weth.address, ); - const takerAssetData = godsUnchainedUtils.encodeBrokerAssetData( - broker.address, - godsUnchained.address, - validator.address, - makerSpecifiedProto, - makerSpecifiedQuality, - ); + const takerAssetData = encodeBrokerAssetData(broker.address, godsUnchained.address, validator.address, { + proto: makerSpecifiedProto, + quality: makerSpecifiedQuality, + }); const orderConfig = { feeRecipientAddress: constants.NULL_ADDRESS, @@ -530,22 +530,16 @@ blockchainTests.resets('Broker <> Gods Unchained integration tests', env => { orders = [ await maker.signOrderAsync({ - takerAssetData: godsUnchainedUtils.encodeBrokerAssetData( - broker.address, - godsUnchained.address, - validator.address, - firstOrderProto, - firstOrderQuality, - ), + takerAssetData: encodeBrokerAssetData(broker.address, godsUnchained.address, validator.address, { + proto: firstOrderProto, + quality: firstOrderQuality, + }), }), await maker.signOrderAsync({ - takerAssetData: godsUnchainedUtils.encodeBrokerAssetData( - broker.address, - godsUnchained.address, - validator.address, - secondOrderProto, - secondOrderQuality, - ), + takerAssetData: encodeBrokerAssetData(broker.address, godsUnchained.address, validator.address, { + proto: secondOrderProto, + quality: secondOrderQuality, + }), }), ]; }); @@ -718,4 +712,30 @@ blockchainTests.resets('Broker <> Gods Unchained integration tests', env => { 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 diff --git a/contracts/integrations/test/stop-limit/chainlink_stop_limit_test.ts b/contracts/integrations/test/stop-limit/chainlink_stop_limit_test.ts index cb77fd9084..cb3928fddf 100644 --- a/contracts/integrations/test/stop-limit/chainlink_stop_limit_test.ts +++ b/contracts/integrations/test/stop-limit/chainlink_stop_limit_test.ts @@ -1,10 +1,22 @@ 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 { SignedOrder } from '@0x/types'; 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 { Actor } from '../framework/actors/base'; @@ -21,6 +33,7 @@ blockchainTests.resets('Chainlink stop-limit order tests', env => { let initialBalances: LocalBalanceStore; let chainLinkAggregator: TestChainlinkAggregatorContract; + let chainlinkStopLimit: ChainlinkStopLimitContract; let maker: Maker; let taker: Taker; @@ -38,7 +51,7 @@ blockchainTests.resets('Chainlink stop-limit order tests', env => { }); const [makerToken, takerToken] = deployment.tokens.erc20; - const chainlinkStopLimit = await ChainlinkStopLimitContract.deployFrom0xArtifactAsync( + chainlinkStopLimit = await ChainlinkStopLimitContract.deployFrom0xArtifactAsync( artifacts.ChainlinkStopLimit, env.provider, env.txDefaults, @@ -55,12 +68,11 @@ blockchainTests.resets('Chainlink stop-limit order tests', env => { [new BigNumber(1), new BigNumber(1)], [ assetDataUtils.encodeERC20AssetData(makerToken.address), - encodeStopLimitStaticCallData( - chainlinkStopLimit.address, - chainLinkAggregator.address, + encodeStopLimitStaticCallData(chainlinkStopLimit.address, { + oracle: chainLinkAggregator.address, minPrice, maxPrice, - ), + }), ], ); @@ -104,50 +116,77 @@ blockchainTests.resets('Chainlink stop-limit order tests', env => { 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); + describe('filling stop-limit orders', () => { + 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); + }); }); - 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); + + 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); + }); }); });