From 0c8bb2e675409c0722b5cb94a8c05f84d68b4bb5 Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Sun, 9 Jun 2019 19:20:47 -0700 Subject: [PATCH] Add unit tests for StaticCallProxy --- contracts/asset-proxy/test/erc1155_proxy.ts | 4 +- .../asset-proxy/test/static_call_proxy.ts | 255 ++++++++++++++++++ contracts/test-utils/src/constants.ts | 1 + packages/types/src/index.ts | 6 +- 4 files changed, 263 insertions(+), 3 deletions(-) create mode 100644 contracts/asset-proxy/test/static_call_proxy.ts diff --git a/contracts/asset-proxy/test/erc1155_proxy.ts b/contracts/asset-proxy/test/erc1155_proxy.ts index 54057eb574..1b0808d115 100644 --- a/contracts/asset-proxy/test/erc1155_proxy.ts +++ b/contracts/asset-proxy/test/erc1155_proxy.ts @@ -121,7 +121,7 @@ describe('ERC1155Proxy', () => { }), ); }); - it('should have an id of 0x9645780d', async () => { + it('should have an id of 0xa7cb5fb7', async () => { const proxyId = await erc1155Proxy.getProxyId.callAsync(); const expectedProxyId = AssetProxyId.ERC1155; expect(proxyId).to.equal(expectedProxyId); @@ -1572,7 +1572,7 @@ describe('ERC1155Proxy', () => { // execute transfer await expectTransactionFailedAsync( erc1155ProxyWrapper.transferFromRawAsync(badTxData, authorized), - RevertReason.InvalidAssetData, + RevertReason.InvalidAssetDataEnd, ); }); it('should revert if length of assetData, excluding the selector, is not a multiple of 32', async () => { diff --git a/contracts/asset-proxy/test/static_call_proxy.ts b/contracts/asset-proxy/test/static_call_proxy.ts new file mode 100644 index 0000000000..1e8da3efed --- /dev/null +++ b/contracts/asset-proxy/test/static_call_proxy.ts @@ -0,0 +1,255 @@ +import { + chaiSetup, + constants, + expectTransactionFailedAsync, + expectTransactionFailedWithoutReasonAsync, + provider, + txDefaults, + web3Wrapper, +} from '@0x/contracts-test-utils'; +import { BlockchainLifecycle } from '@0x/dev-utils'; +import { assetDataUtils } from '@0x/order-utils'; +import { AssetProxyId, RevertReason } from '@0x/types'; +import { AbiEncoder, BigNumber } from '@0x/utils'; +import * as chai from 'chai'; +import * as ethUtil from 'ethereumjs-util'; + +import { artifacts, IAssetProxyContract, StaticCallProxyContract, TestStaticCallTargetContract } from '../src'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +describe('StaticCallProxy', () => { + const amount = constants.ZERO_AMOUNT; + let fromAddress: string; + let toAddress: string; + + let staticCallProxy: IAssetProxyContract; + let staticCallTarget: TestStaticCallTargetContract; + + before(async () => { + await blockchainLifecycle.startAsync(); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + before(async () => { + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + [fromAddress, toAddress] = accounts.slice(0, 2); + const staticCallProxyWithoutTransferFrom = await StaticCallProxyContract.deployFrom0xArtifactAsync( + artifacts.StaticCallProxy, + provider, + txDefaults, + ); + staticCallProxy = new IAssetProxyContract( + artifacts.IAssetProxy.compilerOutput.abi, + staticCallProxyWithoutTransferFrom.address, + provider, + txDefaults, + ); + staticCallTarget = await TestStaticCallTargetContract.deployFrom0xArtifactAsync( + artifacts.TestStaticCallTarget, + provider, + txDefaults, + ); + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + + describe('general', () => { + it('should revert if undefined function is called', async () => { + const undefinedSelector = '0x01020304'; + await expectTransactionFailedWithoutReasonAsync( + web3Wrapper.sendTransactionAsync({ + from: fromAddress, + to: staticCallProxy.address, + value: constants.ZERO_AMOUNT, + data: undefinedSelector, + }), + ); + }); + it('should have an id of 0xc339d10a', async () => { + const proxyId = await staticCallProxy.getProxyId.callAsync(); + const expectedProxyId = AssetProxyId.StaticCall; + expect(proxyId).to.equal(expectedProxyId); + }); + }); + describe('transferFrom', () => { + it('should revert if assetData lies outside the bounds of calldata', async () => { + const staticCallData = staticCallTarget.noInputFunction.getABIEncodedTransactionData(); + const expectedResultHash = constants.KECCAK256_NULL; + const assetData = assetDataUtils.encodeStaticCallAssetData( + staticCallTarget.address, + staticCallData, + expectedResultHash, + ); + const txData = staticCallProxy.transferFrom.getABIEncodedTransactionData( + assetData, + fromAddress, + toAddress, + amount, + ); + const offsetToAssetData = '0000000000000000000000000000000000000000000000000000000000000080'; + const txDataEndBuffer = ethUtil.toBuffer((txData.length - 2) / 2 - 4); + const paddedTxDataEndBuffer = ethUtil.setLengthLeft(txDataEndBuffer, 32); + const invalidOffsetToAssetData = ethUtil.bufferToHex(paddedTxDataEndBuffer).slice(2); + const newAssetData = '0000000000000000000000000000000000000000000000000000000000000304'; + const badTxData = `${txData.replace(offsetToAssetData, invalidOffsetToAssetData)}${newAssetData}`; + await expectTransactionFailedAsync( + web3Wrapper.sendTransactionAsync({ + to: staticCallProxy.address, + from: fromAddress, + data: badTxData, + }), + RevertReason.InvalidAssetDataEnd, + ); + }); + it('should revert if the length of assetData, excluding the proxyId, is not a multiple of 32', async () => { + const staticCallData = staticCallTarget.noInputFunction.getABIEncodedTransactionData(); + const expectedResultHash = constants.KECCAK256_NULL; + const assetData = `${assetDataUtils.encodeStaticCallAssetData( + staticCallTarget.address, + staticCallData, + expectedResultHash, + )}01`; + await expectTransactionFailedAsync( + staticCallProxy.transferFrom.sendTransactionAsync(assetData, fromAddress, toAddress, amount), + RevertReason.InvalidAssetDataLength, + ); + }); + it('should revert if the length of assetData is less than 100 bytes', async () => { + const staticCallData = constants.NULL_BYTES; + const expectedResultHash = constants.KECCAK256_NULL; + const assetData = assetDataUtils + .encodeStaticCallAssetData(staticCallTarget.address, staticCallData, expectedResultHash) + .slice(0, -128); + const assetDataByteLen = (assetData.length - 2) / 2; + expect((assetDataByteLen - 4) % 32).to.equal(0); + await expectTransactionFailedAsync( + staticCallProxy.transferFrom.sendTransactionAsync(assetData, fromAddress, toAddress, amount), + RevertReason.InvalidAssetDataLength, + ); + }); + it('should revert if the offset to `staticCallData` points to outside of assetData', async () => { + const staticCallData = staticCallTarget.noInputFunction.getABIEncodedTransactionData(); + const expectedResultHash = constants.KECCAK256_NULL; + const assetData = assetDataUtils.encodeStaticCallAssetData( + staticCallTarget.address, + staticCallData, + expectedResultHash, + ); + const offsetToStaticCallData = '0000000000000000000000000000000000000000000000000000000000000060'; + const assetDataEndBuffer = ethUtil.toBuffer((assetData.length - 2) / 2 - 4); + const paddedAssetDataEndBuffer = ethUtil.setLengthLeft(assetDataEndBuffer, 32); + const invalidOffsetToStaticCallData = ethUtil.bufferToHex(paddedAssetDataEndBuffer).slice(2); + const newStaticCallData = '0000000000000000000000000000000000000000000000000000000000000304'; + const badAssetData = `${assetData.replace( + offsetToStaticCallData, + invalidOffsetToStaticCallData, + )}${newStaticCallData}`; + await expectTransactionFailedAsync( + staticCallProxy.transferFrom.sendTransactionAsync(badAssetData, fromAddress, toAddress, amount), + RevertReason.InvalidStaticCallDataOffset, + ); + }); + it('should revert if the callTarget attempts to write to state', async () => { + const staticCallData = staticCallTarget.updateState.getABIEncodedTransactionData(); + const expectedResultHash = constants.KECCAK256_NULL; + const assetData = assetDataUtils.encodeStaticCallAssetData( + staticCallTarget.address, + staticCallData, + expectedResultHash, + ); + await expectTransactionFailedWithoutReasonAsync( + staticCallProxy.transferFrom.sendTransactionAsync(assetData, fromAddress, toAddress, amount), + ); + }); + it('should revert with data provided by the callTarget if the staticcall reverts', async () => { + const staticCallData = staticCallTarget.assertEvenNumber.getABIEncodedTransactionData(new BigNumber(1)); + const expectedResultHash = constants.KECCAK256_NULL; + const assetData = assetDataUtils.encodeStaticCallAssetData( + staticCallTarget.address, + staticCallData, + expectedResultHash, + ); + await expectTransactionFailedAsync( + staticCallProxy.transferFrom.sendTransactionAsync(assetData, fromAddress, toAddress, amount), + RevertReason.TargetNotEven, + ); + }); + it('should revert if the hash of the output is different than expected expected', async () => { + const staticCallData = staticCallTarget.isOddNumber.getABIEncodedTransactionData(new BigNumber(0)); + const trueAsBuffer = ethUtil.toBuffer('0x0000000000000000000000000000000000000000000000000000000000000001'); + const expectedResultHash = ethUtil.bufferToHex(ethUtil.sha3(trueAsBuffer)); + const assetData = assetDataUtils.encodeStaticCallAssetData( + staticCallTarget.address, + staticCallData, + expectedResultHash, + ); + await expectTransactionFailedAsync( + staticCallProxy.transferFrom.sendTransactionAsync(assetData, fromAddress, toAddress, amount), + RevertReason.UnexpectedStaticCallResult, + ); + }); + it('should be successful if a function call with no inputs is successful', async () => { + const staticCallData = staticCallTarget.noInputFunction.getABIEncodedTransactionData(); + const expectedResultHash = constants.KECCAK256_NULL; + const assetData = assetDataUtils.encodeStaticCallAssetData( + staticCallTarget.address, + staticCallData, + expectedResultHash, + ); + await staticCallProxy.transferFrom.awaitTransactionSuccessAsync(assetData, fromAddress, toAddress, amount); + }); + it('should be successful if a function call with one static input returns the correct value', async () => { + const staticCallData = staticCallTarget.isOddNumber.getABIEncodedTransactionData(new BigNumber(1)); + const trueAsBuffer = ethUtil.toBuffer('0x0000000000000000000000000000000000000000000000000000000000000001'); + const expectedResultHash = ethUtil.bufferToHex(ethUtil.sha3(trueAsBuffer)); + const assetData = assetDataUtils.encodeStaticCallAssetData( + staticCallTarget.address, + staticCallData, + expectedResultHash, + ); + await staticCallProxy.transferFrom.awaitTransactionSuccessAsync(assetData, fromAddress, toAddress, amount); + }); + it('should be successful if a function with one dynamic input is successful', async () => { + const dynamicInput = '0x0102030405060708'; + const staticCallData = staticCallTarget.dynamicInputFunction.getABIEncodedTransactionData(dynamicInput); + const expectedResultHash = constants.KECCAK256_NULL; + const assetData = assetDataUtils.encodeStaticCallAssetData( + staticCallTarget.address, + staticCallData, + expectedResultHash, + ); + await staticCallProxy.transferFrom.awaitTransactionSuccessAsync(assetData, fromAddress, toAddress, amount); + }); + it('should be successful if a function call returns a complex type', async () => { + const a = new BigNumber(1); + const b = new BigNumber(2); + const staticCallData = staticCallTarget.returnComplexType.getABIEncodedTransactionData(a, b); + const abiEncoder = new AbiEncoder.DynamicBytes({ + name: '', + type: 'bytes', + }); + const aHex = '0000000000000000000000000000000000000000000000000000000000000001'; + const bHex = '0000000000000000000000000000000000000000000000000000000000000002'; + const expectedResults = `${staticCallTarget.address}${aHex}${bHex}`; + const offset = '0000000000000000000000000000000000000000000000000000000000000020'; + const encodedExpectedResultWithOffset = `0x${offset}${abiEncoder.encode(expectedResults).slice(2)}`; + const expectedResultHash = ethUtil.bufferToHex( + ethUtil.sha3(ethUtil.toBuffer(encodedExpectedResultWithOffset)), + ); + const assetData = assetDataUtils.encodeStaticCallAssetData( + staticCallTarget.address, + staticCallData, + expectedResultHash, + ); + await staticCallProxy.transferFrom.awaitTransactionSuccessAsync(assetData, fromAddress, toAddress, amount); + }); + }); +}); diff --git a/contracts/test-utils/src/constants.ts b/contracts/test-utils/src/constants.ts index 199042edc0..f09e554941 100644 --- a/contracts/test-utils/src/constants.ts +++ b/contracts/test-utils/src/constants.ts @@ -70,4 +70,5 @@ export const constants = { 'CANCEL_ORDERS_UP_TO', 'SET_SIGNATURE_VALIDATOR_APPROVAL', ], + KECCAK256_NULL: ethUtil.addHexPrefix(ethUtil.bufferToHex(ethUtil.SHA3_NULL)), }; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 0e70969b2b..e841621650 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -291,7 +291,7 @@ export enum RevertReason { AuctionExpired = 'AUCTION_EXPIRED', AuctionNotStarted = 'AUCTION_NOT_STARTED', AuctionInvalidBeginTime = 'INVALID_BEGIN_TIME', - InvalidAssetData = 'INVALID_ASSET_DATA', + InvalidAssetDataEnd = 'INVALID_ASSET_DATA_END', // Balance Threshold Filter InvalidOrBlockedExchangeSelector = 'INVALID_OR_BLOCKED_EXCHANGE_SELECTOR', BalanceQueryFailed = 'BALANCE_QUERY_FAILED', @@ -317,6 +317,10 @@ export enum RevertReason { InvalidValuesOffset = 'INVALID_VALUES_OFFSET', InvalidDataOffset = 'INVALID_DATA_OFFSET', InvalidAssetDataLength = 'INVALID_ASSET_DATA_LENGTH', + // StaticCall + InvalidStaticCallDataOffset = 'INVALID_STATIC_CALL_DATA_OFFSET', + TargetNotEven = 'TARGET_NOT_EVEN', + UnexpectedStaticCallResult = 'UNEXPECTED_STATIC_CALL_RESULT', } export enum StatusCodes {