ZeroEx: TransformERC20, TokenSpender (#2545)
* `@0x/contracts-utils`: Convert more 0.6 contracts * `@0x/contracts-erc20`: Add solidity 0.6 contracts. * `@0x/utils`: Add new `ZeroExRevertErrors` revert types * `@0x/contracts-zero-ex`: Introduce the `TransformERC20` feature. * `@0x/subproviders`: Update ganache-core. `@0x/web3-wrapper`: Update ganache-core. * `@0x/contracts-zero-ex`: Make `TokenSpender`'s puppet contract a distinct contract type and rename `getTokenSpenderPuppet()` to `getAllowanceTarget()` * `@0x/zero-ex`: Rebase and use "slot" instead of "offset" language in storage buckets. * `@0x/web3-wrapper`: Add `getAccountNonceAsync()` to `Web3Wrapper` * `@0x/contracts-zero-ex`: Revamp TransformERC20. * `@0x/contracts-zero-ex`: Remove `payable` from `IERC20Transformer.transform()` and disable hex capitalization linter rule because of prettier conflicts. * `@0x/contracts-zero-ex`: Use `immutable` owner in `Puppet` instead of `Ownable`. * `@x/utils`: Address review feedback. * `@0x/contracts-zero-ex`: Address review feedback. * `@0x/contracts-utils`: Address review feedback. * `@0x/contracts-zero-ex`: Return deployment nonce in `transform()`. * `@0x/contracts-zero-ex`: Finish returning deployment nonce in `transform()`. * `@0x/contracts-zero-ex`: Fix doc-gen bug. * `@0x/contracts-zero-ex`: Address review comments. * `@0x/utils`: Add `NegativeTransformERC20OutputERror` * `@0x/contracts-zero-ex`: Revert if the taker's output amount decreases. Co-authored-by: Lawrence Forman <me@merklejerk.com>
This commit is contained in:
82
contracts/zero-ex/test/allowance_target_test.ts
Normal file
82
contracts/zero-ex/test/allowance_target_test.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { blockchainTests, constants, expect, randomAddress, verifyEventsFromLogs } from '@0x/contracts-test-utils';
|
||||
import { AuthorizableRevertErrors, hexUtils, StringRevertError } from '@0x/utils';
|
||||
|
||||
import { artifacts } from './artifacts';
|
||||
import { AllowanceTargetContract, TestCallTargetContract, TestCallTargetEvents } from './wrappers';
|
||||
|
||||
blockchainTests.resets('AllowanceTarget', env => {
|
||||
let owner: string;
|
||||
let authority: string;
|
||||
let allowanceTarget: AllowanceTargetContract;
|
||||
let callTarget: TestCallTargetContract;
|
||||
|
||||
before(async () => {
|
||||
[owner, authority] = await env.getAccountAddressesAsync();
|
||||
allowanceTarget = await AllowanceTargetContract.deployFrom0xArtifactAsync(
|
||||
artifacts.AllowanceTarget,
|
||||
env.provider,
|
||||
env.txDefaults,
|
||||
artifacts,
|
||||
);
|
||||
await allowanceTarget.addAuthorizedAddress(authority).awaitTransactionSuccessAsync();
|
||||
callTarget = await TestCallTargetContract.deployFrom0xArtifactAsync(
|
||||
artifacts.TestCallTarget,
|
||||
env.provider,
|
||||
env.txDefaults,
|
||||
artifacts,
|
||||
);
|
||||
});
|
||||
|
||||
const TARGET_RETURN_VALUE = hexUtils.rightPad('0x12345678');
|
||||
const REVERTING_DATA = '0x1337';
|
||||
|
||||
describe('executeCall()', () => {
|
||||
it('non-authority cannot call executeCall()', async () => {
|
||||
const notAuthority = randomAddress();
|
||||
const tx = allowanceTarget
|
||||
.executeCall(randomAddress(), hexUtils.random())
|
||||
.callAsync({ from: notAuthority });
|
||||
return expect(tx).to.revertWith(new AuthorizableRevertErrors.SenderNotAuthorizedError(notAuthority));
|
||||
});
|
||||
|
||||
it('authority can call executeCall()', async () => {
|
||||
const targetData = hexUtils.random(128);
|
||||
const receipt = await allowanceTarget
|
||||
.executeCall(callTarget.address, targetData)
|
||||
.awaitTransactionSuccessAsync({ from: authority });
|
||||
verifyEventsFromLogs(
|
||||
receipt.logs,
|
||||
[
|
||||
{
|
||||
context: callTarget.address,
|
||||
sender: allowanceTarget.address,
|
||||
data: targetData,
|
||||
value: constants.ZERO_AMOUNT,
|
||||
},
|
||||
],
|
||||
TestCallTargetEvents.CallTargetCalled,
|
||||
);
|
||||
});
|
||||
|
||||
it('AllowanceTarget returns call result', async () => {
|
||||
const result = await allowanceTarget
|
||||
.executeCall(callTarget.address, hexUtils.random(128))
|
||||
.callAsync({ from: authority });
|
||||
expect(result).to.eq(TARGET_RETURN_VALUE);
|
||||
});
|
||||
|
||||
it('AllowanceTarget returns raw call revert', async () => {
|
||||
const tx = allowanceTarget.executeCall(callTarget.address, REVERTING_DATA).callAsync({ from: authority });
|
||||
return expect(tx).to.revertWith(new StringRevertError('TestCallTarget/REVERT'));
|
||||
});
|
||||
|
||||
it('AllowanceTarget cannot receive ETH', async () => {
|
||||
const tx = env.web3Wrapper.sendTransactionAsync({
|
||||
to: allowanceTarget.address,
|
||||
from: owner,
|
||||
value: 0,
|
||||
});
|
||||
return expect(tx).to.eventually.be.rejected();
|
||||
});
|
||||
});
|
||||
});
|
@@ -5,16 +5,25 @@
|
||||
*/
|
||||
import { ContractArtifact } from 'ethereum-types';
|
||||
|
||||
import * as AllowanceTarget from '../test/generated-artifacts/AllowanceTarget.json';
|
||||
import * as Bootstrap from '../test/generated-artifacts/Bootstrap.json';
|
||||
import * as FixinCommon from '../test/generated-artifacts/FixinCommon.json';
|
||||
import * as FlashWallet from '../test/generated-artifacts/FlashWallet.json';
|
||||
import * as FullMigration from '../test/generated-artifacts/FullMigration.json';
|
||||
import * as IAllowanceTarget from '../test/generated-artifacts/IAllowanceTarget.json';
|
||||
import * as IBootstrap from '../test/generated-artifacts/IBootstrap.json';
|
||||
import * as IERC20Transformer from '../test/generated-artifacts/IERC20Transformer.json';
|
||||
import * as IFeature from '../test/generated-artifacts/IFeature.json';
|
||||
import * as IFlashWallet from '../test/generated-artifacts/IFlashWallet.json';
|
||||
import * as InitialMigration from '../test/generated-artifacts/InitialMigration.json';
|
||||
import * as IOwnable from '../test/generated-artifacts/IOwnable.json';
|
||||
import * as ISimpleFunctionRegistry from '../test/generated-artifacts/ISimpleFunctionRegistry.json';
|
||||
import * as ITestSimpleFunctionRegistryFeature from '../test/generated-artifacts/ITestSimpleFunctionRegistryFeature.json';
|
||||
import * as ITokenSpender from '../test/generated-artifacts/ITokenSpender.json';
|
||||
import * as ITransformERC20 from '../test/generated-artifacts/ITransformERC20.json';
|
||||
import * as LibBootstrap from '../test/generated-artifacts/LibBootstrap.json';
|
||||
import * as LibCommonRichErrors from '../test/generated-artifacts/LibCommonRichErrors.json';
|
||||
import * as LibERC20Transformer from '../test/generated-artifacts/LibERC20Transformer.json';
|
||||
import * as LibMigrate from '../test/generated-artifacts/LibMigrate.json';
|
||||
import * as LibOwnableRichErrors from '../test/generated-artifacts/LibOwnableRichErrors.json';
|
||||
import * as LibOwnableStorage from '../test/generated-artifacts/LibOwnableStorage.json';
|
||||
@@ -22,14 +31,28 @@ import * as LibProxyRichErrors from '../test/generated-artifacts/LibProxyRichErr
|
||||
import * as LibProxyStorage from '../test/generated-artifacts/LibProxyStorage.json';
|
||||
import * as LibSimpleFunctionRegistryRichErrors from '../test/generated-artifacts/LibSimpleFunctionRegistryRichErrors.json';
|
||||
import * as LibSimpleFunctionRegistryStorage from '../test/generated-artifacts/LibSimpleFunctionRegistryStorage.json';
|
||||
import * as LibSpenderRichErrors from '../test/generated-artifacts/LibSpenderRichErrors.json';
|
||||
import * as LibStorage from '../test/generated-artifacts/LibStorage.json';
|
||||
import * as LibTokenSpenderStorage from '../test/generated-artifacts/LibTokenSpenderStorage.json';
|
||||
import * as LibTransformERC20RichErrors from '../test/generated-artifacts/LibTransformERC20RichErrors.json';
|
||||
import * as LibTransformERC20Storage from '../test/generated-artifacts/LibTransformERC20Storage.json';
|
||||
import * as LibWalletRichErrors from '../test/generated-artifacts/LibWalletRichErrors.json';
|
||||
import * as Ownable from '../test/generated-artifacts/Ownable.json';
|
||||
import * as SimpleFunctionRegistry from '../test/generated-artifacts/SimpleFunctionRegistry.json';
|
||||
import * as TestCallTarget from '../test/generated-artifacts/TestCallTarget.json';
|
||||
import * as TestFullMigration from '../test/generated-artifacts/TestFullMigration.json';
|
||||
import * as TestInitialMigration from '../test/generated-artifacts/TestInitialMigration.json';
|
||||
import * as TestMigrator from '../test/generated-artifacts/TestMigrator.json';
|
||||
import * as TestMintableERC20Token from '../test/generated-artifacts/TestMintableERC20Token.json';
|
||||
import * as TestMintTokenERC20Transformer from '../test/generated-artifacts/TestMintTokenERC20Transformer.json';
|
||||
import * as TestSimpleFunctionRegistryFeatureImpl1 from '../test/generated-artifacts/TestSimpleFunctionRegistryFeatureImpl1.json';
|
||||
import * as TestSimpleFunctionRegistryFeatureImpl2 from '../test/generated-artifacts/TestSimpleFunctionRegistryFeatureImpl2.json';
|
||||
import * as TestTokenSpender from '../test/generated-artifacts/TestTokenSpender.json';
|
||||
import * as TestTokenSpenderERC20Token from '../test/generated-artifacts/TestTokenSpenderERC20Token.json';
|
||||
import * as TestTransformERC20 from '../test/generated-artifacts/TestTransformERC20.json';
|
||||
import * as TestZeroExFeature from '../test/generated-artifacts/TestZeroExFeature.json';
|
||||
import * as TokenSpender from '../test/generated-artifacts/TokenSpender.json';
|
||||
import * as TransformERC20 from '../test/generated-artifacts/TransformERC20.json';
|
||||
import * as ZeroEx from '../test/generated-artifacts/ZeroEx.json';
|
||||
export const artifacts = {
|
||||
ZeroEx: ZeroEx as ContractArtifact,
|
||||
@@ -37,14 +60,26 @@ export const artifacts = {
|
||||
LibOwnableRichErrors: LibOwnableRichErrors as ContractArtifact,
|
||||
LibProxyRichErrors: LibProxyRichErrors as ContractArtifact,
|
||||
LibSimpleFunctionRegistryRichErrors: LibSimpleFunctionRegistryRichErrors as ContractArtifact,
|
||||
LibSpenderRichErrors: LibSpenderRichErrors as ContractArtifact,
|
||||
LibTransformERC20RichErrors: LibTransformERC20RichErrors as ContractArtifact,
|
||||
LibWalletRichErrors: LibWalletRichErrors as ContractArtifact,
|
||||
AllowanceTarget: AllowanceTarget as ContractArtifact,
|
||||
FlashWallet: FlashWallet as ContractArtifact,
|
||||
IAllowanceTarget: IAllowanceTarget as ContractArtifact,
|
||||
IFlashWallet: IFlashWallet as ContractArtifact,
|
||||
Bootstrap: Bootstrap as ContractArtifact,
|
||||
IBootstrap: IBootstrap as ContractArtifact,
|
||||
IFeature: IFeature as ContractArtifact,
|
||||
IOwnable: IOwnable as ContractArtifact,
|
||||
ISimpleFunctionRegistry: ISimpleFunctionRegistry as ContractArtifact,
|
||||
ITokenSpender: ITokenSpender as ContractArtifact,
|
||||
ITransformERC20: ITransformERC20 as ContractArtifact,
|
||||
Ownable: Ownable as ContractArtifact,
|
||||
SimpleFunctionRegistry: SimpleFunctionRegistry as ContractArtifact,
|
||||
TokenSpender: TokenSpender as ContractArtifact,
|
||||
TransformERC20: TransformERC20 as ContractArtifact,
|
||||
FixinCommon: FixinCommon as ContractArtifact,
|
||||
FullMigration: FullMigration as ContractArtifact,
|
||||
InitialMigration: InitialMigration as ContractArtifact,
|
||||
LibBootstrap: LibBootstrap as ContractArtifact,
|
||||
LibMigrate: LibMigrate as ContractArtifact,
|
||||
@@ -52,10 +87,21 @@ export const artifacts = {
|
||||
LibProxyStorage: LibProxyStorage as ContractArtifact,
|
||||
LibSimpleFunctionRegistryStorage: LibSimpleFunctionRegistryStorage as ContractArtifact,
|
||||
LibStorage: LibStorage as ContractArtifact,
|
||||
LibTokenSpenderStorage: LibTokenSpenderStorage as ContractArtifact,
|
||||
LibTransformERC20Storage: LibTransformERC20Storage as ContractArtifact,
|
||||
IERC20Transformer: IERC20Transformer as ContractArtifact,
|
||||
LibERC20Transformer: LibERC20Transformer as ContractArtifact,
|
||||
ITestSimpleFunctionRegistryFeature: ITestSimpleFunctionRegistryFeature as ContractArtifact,
|
||||
TestCallTarget: TestCallTarget as ContractArtifact,
|
||||
TestFullMigration: TestFullMigration as ContractArtifact,
|
||||
TestInitialMigration: TestInitialMigration as ContractArtifact,
|
||||
TestMigrator: TestMigrator as ContractArtifact,
|
||||
TestMintTokenERC20Transformer: TestMintTokenERC20Transformer as ContractArtifact,
|
||||
TestMintableERC20Token: TestMintableERC20Token as ContractArtifact,
|
||||
TestSimpleFunctionRegistryFeatureImpl1: TestSimpleFunctionRegistryFeatureImpl1 as ContractArtifact,
|
||||
TestSimpleFunctionRegistryFeatureImpl2: TestSimpleFunctionRegistryFeatureImpl2 as ContractArtifact,
|
||||
TestTokenSpender: TestTokenSpender as ContractArtifact,
|
||||
TestTokenSpenderERC20Token: TestTokenSpenderERC20Token as ContractArtifact,
|
||||
TestTransformERC20: TestTransformERC20 as ContractArtifact,
|
||||
TestZeroExFeature: TestZeroExFeature as ContractArtifact,
|
||||
};
|
||||
|
141
contracts/zero-ex/test/features/token_spender_test.ts
Normal file
141
contracts/zero-ex/test/features/token_spender_test.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import {
|
||||
blockchainTests,
|
||||
expect,
|
||||
getRandomInteger,
|
||||
randomAddress,
|
||||
verifyEventsFromLogs,
|
||||
} from '@0x/contracts-test-utils';
|
||||
import { BigNumber, hexUtils, StringRevertError, ZeroExRevertErrors } from '@0x/utils';
|
||||
|
||||
import { artifacts } from '../artifacts';
|
||||
import { abis } from '../utils/abis';
|
||||
import { fullMigrateAsync } from '../utils/migration';
|
||||
import {
|
||||
TestTokenSpenderERC20TokenContract,
|
||||
TestTokenSpenderERC20TokenEvents,
|
||||
TokenSpenderContract,
|
||||
ZeroExContract,
|
||||
} from '../wrappers';
|
||||
|
||||
blockchainTests.resets('TokenSpender feature', env => {
|
||||
let zeroEx: ZeroExContract;
|
||||
let feature: TokenSpenderContract;
|
||||
let token: TestTokenSpenderERC20TokenContract;
|
||||
let allowanceTarget: string;
|
||||
|
||||
before(async () => {
|
||||
const [owner] = await env.getAccountAddressesAsync();
|
||||
zeroEx = await fullMigrateAsync(owner, env.provider, env.txDefaults, {
|
||||
tokenSpender: await TokenSpenderContract.deployFrom0xArtifactAsync(
|
||||
artifacts.TestTokenSpender,
|
||||
env.provider,
|
||||
env.txDefaults,
|
||||
artifacts,
|
||||
),
|
||||
});
|
||||
feature = new TokenSpenderContract(zeroEx.address, env.provider, env.txDefaults, abis);
|
||||
token = await TestTokenSpenderERC20TokenContract.deployFrom0xArtifactAsync(
|
||||
artifacts.TestTokenSpenderERC20Token,
|
||||
env.provider,
|
||||
env.txDefaults,
|
||||
artifacts,
|
||||
);
|
||||
allowanceTarget = await feature.getAllowanceTarget().callAsync();
|
||||
});
|
||||
|
||||
describe('_spendERC20Tokens()', () => {
|
||||
const EMPTY_RETURN_AMOUNT = 1337;
|
||||
const FALSE_RETURN_AMOUNT = 1338;
|
||||
const REVERT_RETURN_AMOUNT = 1339;
|
||||
|
||||
it('_spendERC20Tokens() successfully calls compliant ERC20 token', async () => {
|
||||
const tokenFrom = randomAddress();
|
||||
const tokenTo = randomAddress();
|
||||
const tokenAmount = new BigNumber(123456);
|
||||
const receipt = await feature
|
||||
._spendERC20Tokens(token.address, tokenFrom, tokenTo, tokenAmount)
|
||||
.awaitTransactionSuccessAsync();
|
||||
verifyEventsFromLogs(
|
||||
receipt.logs,
|
||||
[
|
||||
{
|
||||
sender: allowanceTarget,
|
||||
from: tokenFrom,
|
||||
to: tokenTo,
|
||||
amount: tokenAmount,
|
||||
},
|
||||
],
|
||||
TestTokenSpenderERC20TokenEvents.TransferFromCalled,
|
||||
);
|
||||
});
|
||||
|
||||
it('_spendERC20Tokens() successfully calls non-compliant ERC20 token', async () => {
|
||||
const tokenFrom = randomAddress();
|
||||
const tokenTo = randomAddress();
|
||||
const tokenAmount = new BigNumber(EMPTY_RETURN_AMOUNT);
|
||||
const receipt = await feature
|
||||
._spendERC20Tokens(token.address, tokenFrom, tokenTo, tokenAmount)
|
||||
.awaitTransactionSuccessAsync();
|
||||
verifyEventsFromLogs(
|
||||
receipt.logs,
|
||||
[
|
||||
{
|
||||
sender: allowanceTarget,
|
||||
from: tokenFrom,
|
||||
to: tokenTo,
|
||||
amount: tokenAmount,
|
||||
},
|
||||
],
|
||||
TestTokenSpenderERC20TokenEvents.TransferFromCalled,
|
||||
);
|
||||
});
|
||||
|
||||
it('_spendERC20Tokens() reverts if ERC20 token reverts', async () => {
|
||||
const tokenFrom = randomAddress();
|
||||
const tokenTo = randomAddress();
|
||||
const tokenAmount = new BigNumber(REVERT_RETURN_AMOUNT);
|
||||
const tx = feature
|
||||
._spendERC20Tokens(token.address, tokenFrom, tokenTo, tokenAmount)
|
||||
.awaitTransactionSuccessAsync();
|
||||
const expectedError = new ZeroExRevertErrors.Spender.SpenderERC20TransferFromFailedError(
|
||||
token.address,
|
||||
tokenFrom,
|
||||
tokenTo,
|
||||
tokenAmount,
|
||||
new StringRevertError('TestTokenSpenderERC20Token/Revert').encode(),
|
||||
);
|
||||
return expect(tx).to.revertWith(expectedError);
|
||||
});
|
||||
|
||||
it('_spendERC20Tokens() reverts if ERC20 token returns false', async () => {
|
||||
const tokenFrom = randomAddress();
|
||||
const tokenTo = randomAddress();
|
||||
const tokenAmount = new BigNumber(FALSE_RETURN_AMOUNT);
|
||||
const tx = feature
|
||||
._spendERC20Tokens(token.address, tokenFrom, tokenTo, tokenAmount)
|
||||
.awaitTransactionSuccessAsync();
|
||||
return expect(tx).to.revertWith(
|
||||
new ZeroExRevertErrors.Spender.SpenderERC20TransferFromFailedError(
|
||||
token.address,
|
||||
tokenFrom,
|
||||
tokenTo,
|
||||
tokenAmount,
|
||||
hexUtils.leftPad(0),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSpendableERC20BalanceOf()', () => {
|
||||
it("returns the minimum of the owner's balance and allowance", async () => {
|
||||
const balance = getRandomInteger(1, '1e18');
|
||||
const allowance = getRandomInteger(1, '1e18');
|
||||
const tokenOwner = randomAddress();
|
||||
await token
|
||||
.setBalanceAndAllowanceOf(tokenOwner, balance, allowanceTarget, allowance)
|
||||
.awaitTransactionSuccessAsync();
|
||||
const spendableBalance = await feature.getSpendableERC20BalanceOf(token.address, tokenOwner).callAsync();
|
||||
expect(spendableBalance).to.bignumber.eq(BigNumber.min(balance, allowance));
|
||||
});
|
||||
});
|
||||
});
|
526
contracts/zero-ex/test/features/transform_erc20_test.ts
Normal file
526
contracts/zero-ex/test/features/transform_erc20_test.ts
Normal file
@@ -0,0 +1,526 @@
|
||||
import {
|
||||
blockchainTests,
|
||||
constants,
|
||||
expect,
|
||||
getRandomInteger,
|
||||
getRandomPortion,
|
||||
Numberish,
|
||||
randomAddress,
|
||||
verifyEventsFromLogs,
|
||||
} from '@0x/contracts-test-utils';
|
||||
import { AbiEncoder, hexUtils, ZeroExRevertErrors } from '@0x/utils';
|
||||
|
||||
import { getRLPEncodedAccountNonceAsync } from '../../src/nonce_utils';
|
||||
import { artifacts } from '../artifacts';
|
||||
import { abis } from '../utils/abis';
|
||||
import { fullMigrateAsync } from '../utils/migration';
|
||||
import {
|
||||
FlashWalletContract,
|
||||
ITokenSpenderContract,
|
||||
TestMintableERC20TokenContract,
|
||||
TestMintTokenERC20TransformerContract,
|
||||
TestMintTokenERC20TransformerEvents,
|
||||
TransformERC20Contract,
|
||||
TransformERC20Events,
|
||||
ZeroExContract,
|
||||
} from '../wrappers';
|
||||
|
||||
blockchainTests.resets('TransformERC20 feature', env => {
|
||||
let taker: string;
|
||||
let transformerDeployer: string;
|
||||
let zeroEx: ZeroExContract;
|
||||
let feature: TransformERC20Contract;
|
||||
let wallet: FlashWalletContract;
|
||||
let allowanceTarget: string;
|
||||
|
||||
before(async () => {
|
||||
let owner;
|
||||
[owner, taker, transformerDeployer] = await env.getAccountAddressesAsync();
|
||||
zeroEx = await fullMigrateAsync(owner, env.provider, env.txDefaults, {
|
||||
transformERC20: await TransformERC20Contract.deployFrom0xArtifactAsync(
|
||||
artifacts.TestTransformERC20,
|
||||
env.provider,
|
||||
env.txDefaults,
|
||||
artifacts,
|
||||
transformerDeployer,
|
||||
),
|
||||
});
|
||||
feature = new TransformERC20Contract(zeroEx.address, env.provider, env.txDefaults, abis);
|
||||
wallet = new FlashWalletContract(await feature.getTransformWallet().callAsync(), env.provider, env.txDefaults);
|
||||
allowanceTarget = await new ITokenSpenderContract(zeroEx.address, env.provider, env.txDefaults)
|
||||
.getAllowanceTarget()
|
||||
.callAsync();
|
||||
});
|
||||
|
||||
const { MAX_UINT256, NULL_BYTES, ZERO_AMOUNT } = constants;
|
||||
|
||||
describe('wallets', () => {
|
||||
it('createTransformWallet() replaces the current wallet', async () => {
|
||||
const newWalletAddress = await feature.createTransformWallet().callAsync();
|
||||
expect(newWalletAddress).to.not.eq(wallet.address);
|
||||
await feature.createTransformWallet().awaitTransactionSuccessAsync();
|
||||
return expect(feature.getTransformWallet().callAsync()).to.eventually.eq(newWalletAddress);
|
||||
});
|
||||
});
|
||||
|
||||
describe('_transformERC20()', () => {
|
||||
let inputToken: TestMintableERC20TokenContract;
|
||||
let outputToken: TestMintableERC20TokenContract;
|
||||
let mintTransformer: TestMintTokenERC20TransformerContract;
|
||||
let rlpNonce: string;
|
||||
|
||||
before(async () => {
|
||||
inputToken = await TestMintableERC20TokenContract.deployFrom0xArtifactAsync(
|
||||
artifacts.TestMintableERC20Token,
|
||||
env.provider,
|
||||
env.txDefaults,
|
||||
artifacts,
|
||||
);
|
||||
outputToken = await TestMintableERC20TokenContract.deployFrom0xArtifactAsync(
|
||||
artifacts.TestMintableERC20Token,
|
||||
env.provider,
|
||||
env.txDefaults,
|
||||
artifacts,
|
||||
);
|
||||
rlpNonce = await getRLPEncodedAccountNonceAsync(env.web3Wrapper, transformerDeployer);
|
||||
mintTransformer = await TestMintTokenERC20TransformerContract.deployFrom0xArtifactAsync(
|
||||
artifacts.TestMintTokenERC20Transformer,
|
||||
env.provider,
|
||||
{
|
||||
...env.txDefaults,
|
||||
from: transformerDeployer,
|
||||
},
|
||||
artifacts,
|
||||
);
|
||||
await inputToken.approve(allowanceTarget, MAX_UINT256).awaitTransactionSuccessAsync({ from: taker });
|
||||
});
|
||||
|
||||
interface Transformation {
|
||||
transformer: string;
|
||||
data: string;
|
||||
}
|
||||
|
||||
const transformDataEncoder = AbiEncoder.create([
|
||||
{
|
||||
name: 'data',
|
||||
type: 'tuple',
|
||||
components: [
|
||||
{ name: 'inputToken', type: 'address' },
|
||||
{ name: 'outputToken', type: 'address' },
|
||||
{ name: 'burnAmount', type: 'uint256' },
|
||||
{ name: 'mintAmount', type: 'uint256' },
|
||||
{ name: 'feeAmount', type: 'uint256' },
|
||||
{ name: 'deploymentNonce', type: 'bytes' },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
function createMintTokenTransformation(
|
||||
opts: Partial<{
|
||||
transformer: string;
|
||||
outputTokenAddress: string;
|
||||
inputTokenAddress: string;
|
||||
inputTokenBurnAmunt: Numberish;
|
||||
outputTokenMintAmount: Numberish;
|
||||
outputTokenFeeAmount: Numberish;
|
||||
rlpNonce: string;
|
||||
}> = {},
|
||||
): Transformation {
|
||||
const _opts = {
|
||||
rlpNonce,
|
||||
outputTokenAddress: outputToken.address,
|
||||
inputTokenAddress: inputToken.address,
|
||||
inputTokenBurnAmunt: ZERO_AMOUNT,
|
||||
outputTokenMintAmount: ZERO_AMOUNT,
|
||||
outputTokenFeeAmount: ZERO_AMOUNT,
|
||||
transformer: mintTransformer.address,
|
||||
...opts,
|
||||
};
|
||||
return {
|
||||
transformer: _opts.transformer,
|
||||
data: transformDataEncoder.encode([
|
||||
{
|
||||
inputToken: _opts.inputTokenAddress,
|
||||
outputToken: _opts.outputTokenAddress,
|
||||
burnAmount: _opts.inputTokenBurnAmunt,
|
||||
mintAmount: _opts.outputTokenMintAmount,
|
||||
feeAmount: _opts.outputTokenFeeAmount,
|
||||
deploymentNonce: _opts.rlpNonce,
|
||||
},
|
||||
]),
|
||||
};
|
||||
}
|
||||
|
||||
it("succeeds if taker's output token balance increases by exactly minOutputTokenAmount", async () => {
|
||||
const startingOutputTokenBalance = getRandomInteger(0, '100e18');
|
||||
const startingInputTokenBalance = getRandomInteger(0, '100e18');
|
||||
await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync();
|
||||
await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync();
|
||||
const inputTokenAmount = getRandomPortion(startingInputTokenBalance);
|
||||
const minOutputTokenAmount = getRandomInteger(1, '1e18');
|
||||
const outputTokenMintAmount = minOutputTokenAmount;
|
||||
const callValue = getRandomInteger(1, '1e18');
|
||||
const callDataHash = hexUtils.random();
|
||||
const transformation = createMintTokenTransformation({
|
||||
outputTokenMintAmount,
|
||||
inputTokenBurnAmunt: inputTokenAmount,
|
||||
});
|
||||
const receipt = await feature
|
||||
._transformERC20(
|
||||
callDataHash,
|
||||
taker,
|
||||
inputToken.address,
|
||||
outputToken.address,
|
||||
inputTokenAmount,
|
||||
minOutputTokenAmount,
|
||||
[transformation],
|
||||
)
|
||||
.awaitTransactionSuccessAsync({ value: callValue });
|
||||
verifyEventsFromLogs(
|
||||
receipt.logs,
|
||||
[
|
||||
{
|
||||
taker,
|
||||
inputTokenAmount,
|
||||
outputTokenAmount: outputTokenMintAmount,
|
||||
inputToken: inputToken.address,
|
||||
outputToken: outputToken.address,
|
||||
},
|
||||
],
|
||||
TransformERC20Events.TransformedERC20,
|
||||
);
|
||||
verifyEventsFromLogs(
|
||||
receipt.logs,
|
||||
[
|
||||
{
|
||||
callDataHash,
|
||||
taker,
|
||||
context: wallet.address,
|
||||
caller: zeroEx.address,
|
||||
data: transformation.data,
|
||||
inputTokenBalance: inputTokenAmount,
|
||||
ethBalance: callValue,
|
||||
},
|
||||
],
|
||||
TestMintTokenERC20TransformerEvents.MintTransform,
|
||||
);
|
||||
});
|
||||
|
||||
const ETH_TOKEN_ADDRESS = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee';
|
||||
|
||||
it("succeeds if taker's output token balance increases by exactly minOutputTokenAmount, with ETH", async () => {
|
||||
const startingInputTokenBalance = getRandomInteger(0, '100e18');
|
||||
await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync();
|
||||
const inputTokenAmount = getRandomPortion(startingInputTokenBalance);
|
||||
const minOutputTokenAmount = getRandomInteger(1, '1e18');
|
||||
const outputTokenMintAmount = minOutputTokenAmount;
|
||||
const callValue = outputTokenMintAmount.times(2);
|
||||
const callDataHash = hexUtils.random();
|
||||
const transformation = createMintTokenTransformation({
|
||||
outputTokenMintAmount,
|
||||
inputTokenBurnAmunt: inputTokenAmount,
|
||||
outputTokenAddress: ETH_TOKEN_ADDRESS,
|
||||
});
|
||||
const startingOutputTokenBalance = await env.web3Wrapper.getBalanceInWeiAsync(taker);
|
||||
const receipt = await feature
|
||||
._transformERC20(
|
||||
callDataHash,
|
||||
taker,
|
||||
inputToken.address,
|
||||
ETH_TOKEN_ADDRESS,
|
||||
inputTokenAmount,
|
||||
minOutputTokenAmount,
|
||||
[transformation],
|
||||
)
|
||||
.awaitTransactionSuccessAsync({ value: callValue });
|
||||
verifyEventsFromLogs(
|
||||
receipt.logs,
|
||||
[
|
||||
{
|
||||
taker,
|
||||
inputTokenAmount,
|
||||
outputTokenAmount: outputTokenMintAmount,
|
||||
inputToken: inputToken.address,
|
||||
outputToken: ETH_TOKEN_ADDRESS,
|
||||
},
|
||||
],
|
||||
TransformERC20Events.TransformedERC20,
|
||||
);
|
||||
verifyEventsFromLogs(
|
||||
receipt.logs,
|
||||
[
|
||||
{
|
||||
callDataHash,
|
||||
taker,
|
||||
context: wallet.address,
|
||||
caller: zeroEx.address,
|
||||
data: transformation.data,
|
||||
inputTokenBalance: inputTokenAmount,
|
||||
ethBalance: callValue,
|
||||
},
|
||||
],
|
||||
TestMintTokenERC20TransformerEvents.MintTransform,
|
||||
);
|
||||
expect(await env.web3Wrapper.getBalanceInWeiAsync(taker)).to.bignumber.eq(
|
||||
startingOutputTokenBalance.plus(outputTokenMintAmount),
|
||||
);
|
||||
});
|
||||
|
||||
it("succeeds if taker's output token balance increases by more than minOutputTokenAmount", async () => {
|
||||
const startingOutputTokenBalance = getRandomInteger(0, '100e18');
|
||||
const startingInputTokenBalance = getRandomInteger(0, '100e18');
|
||||
await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync();
|
||||
await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync();
|
||||
const inputTokenAmount = getRandomPortion(startingInputTokenBalance);
|
||||
const minOutputTokenAmount = getRandomInteger(1, '1e18');
|
||||
const outputTokenMintAmount = minOutputTokenAmount.plus(1);
|
||||
const callValue = getRandomInteger(1, '1e18');
|
||||
const callDataHash = hexUtils.random();
|
||||
const transformation = createMintTokenTransformation({
|
||||
outputTokenMintAmount,
|
||||
inputTokenBurnAmunt: inputTokenAmount,
|
||||
});
|
||||
const receipt = await feature
|
||||
._transformERC20(
|
||||
callDataHash,
|
||||
taker,
|
||||
inputToken.address,
|
||||
outputToken.address,
|
||||
inputTokenAmount,
|
||||
minOutputTokenAmount,
|
||||
[transformation],
|
||||
)
|
||||
.awaitTransactionSuccessAsync({ value: callValue });
|
||||
verifyEventsFromLogs(
|
||||
receipt.logs,
|
||||
[
|
||||
{
|
||||
taker,
|
||||
inputTokenAmount,
|
||||
outputTokenAmount: outputTokenMintAmount,
|
||||
inputToken: inputToken.address,
|
||||
outputToken: outputToken.address,
|
||||
},
|
||||
],
|
||||
TransformERC20Events.TransformedERC20,
|
||||
);
|
||||
verifyEventsFromLogs(
|
||||
receipt.logs,
|
||||
[
|
||||
{
|
||||
callDataHash,
|
||||
taker,
|
||||
context: wallet.address,
|
||||
caller: zeroEx.address,
|
||||
data: transformation.data,
|
||||
inputTokenBalance: inputTokenAmount,
|
||||
ethBalance: callValue,
|
||||
},
|
||||
],
|
||||
TestMintTokenERC20TransformerEvents.MintTransform,
|
||||
);
|
||||
});
|
||||
|
||||
it("throws if taker's output token balance increases by less than minOutputTokenAmount", async () => {
|
||||
const startingOutputTokenBalance = getRandomInteger(0, '100e18');
|
||||
const startingInputTokenBalance = getRandomInteger(0, '100e18');
|
||||
await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync();
|
||||
await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync();
|
||||
const inputTokenAmount = getRandomPortion(startingInputTokenBalance);
|
||||
const minOutputTokenAmount = getRandomInteger(1, '1e18');
|
||||
const outputTokenMintAmount = minOutputTokenAmount.minus(1);
|
||||
const callValue = getRandomInteger(1, '1e18');
|
||||
const tx = feature
|
||||
._transformERC20(
|
||||
hexUtils.random(),
|
||||
taker,
|
||||
inputToken.address,
|
||||
outputToken.address,
|
||||
inputTokenAmount,
|
||||
minOutputTokenAmount,
|
||||
[
|
||||
createMintTokenTransformation({
|
||||
outputTokenMintAmount,
|
||||
inputTokenBurnAmunt: inputTokenAmount,
|
||||
}),
|
||||
],
|
||||
)
|
||||
.awaitTransactionSuccessAsync({ value: callValue });
|
||||
const expectedError = new ZeroExRevertErrors.TransformERC20.IncompleteTransformERC20Error(
|
||||
outputToken.address,
|
||||
outputTokenMintAmount,
|
||||
minOutputTokenAmount,
|
||||
);
|
||||
return expect(tx).to.revertWith(expectedError);
|
||||
});
|
||||
|
||||
it("throws if taker's output token balance decreases", async () => {
|
||||
const startingOutputTokenBalance = getRandomInteger(0, '100e18');
|
||||
const startingInputTokenBalance = getRandomInteger(0, '100e18');
|
||||
await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync();
|
||||
await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync();
|
||||
const inputTokenAmount = getRandomPortion(startingInputTokenBalance);
|
||||
const minOutputTokenAmount = ZERO_AMOUNT;
|
||||
const outputTokenFeeAmount = 1;
|
||||
const callValue = getRandomInteger(1, '1e18');
|
||||
const tx = feature
|
||||
._transformERC20(
|
||||
hexUtils.random(),
|
||||
taker,
|
||||
inputToken.address,
|
||||
outputToken.address,
|
||||
inputTokenAmount,
|
||||
minOutputTokenAmount,
|
||||
[
|
||||
createMintTokenTransformation({
|
||||
outputTokenFeeAmount,
|
||||
inputTokenBurnAmunt: inputTokenAmount,
|
||||
}),
|
||||
],
|
||||
)
|
||||
.awaitTransactionSuccessAsync({ value: callValue });
|
||||
const expectedError = new ZeroExRevertErrors.TransformERC20.NegativeTransformERC20OutputError(
|
||||
outputToken.address,
|
||||
outputTokenFeeAmount,
|
||||
);
|
||||
return expect(tx).to.revertWith(expectedError);
|
||||
});
|
||||
|
||||
it('can call multiple transformers', async () => {
|
||||
const startingOutputTokenBalance = getRandomInteger(0, '100e18');
|
||||
const startingInputTokenBalance = getRandomInteger(2, '100e18');
|
||||
await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync();
|
||||
await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync();
|
||||
const inputTokenAmount = getRandomPortion(startingInputTokenBalance);
|
||||
const minOutputTokenAmount = getRandomInteger(2, '1e18');
|
||||
const outputTokenMintAmount = minOutputTokenAmount;
|
||||
const callValue = getRandomInteger(1, '1e18');
|
||||
const callDataHash = hexUtils.random();
|
||||
// Split the total minting between two transformers.
|
||||
const transformations = [
|
||||
createMintTokenTransformation({
|
||||
inputTokenBurnAmunt: 1,
|
||||
outputTokenMintAmount: 1,
|
||||
}),
|
||||
createMintTokenTransformation({
|
||||
inputTokenBurnAmunt: inputTokenAmount.minus(1),
|
||||
outputTokenMintAmount: outputTokenMintAmount.minus(1),
|
||||
}),
|
||||
];
|
||||
const receipt = await feature
|
||||
._transformERC20(
|
||||
callDataHash,
|
||||
taker,
|
||||
inputToken.address,
|
||||
outputToken.address,
|
||||
inputTokenAmount,
|
||||
minOutputTokenAmount,
|
||||
transformations,
|
||||
)
|
||||
.awaitTransactionSuccessAsync({ value: callValue });
|
||||
verifyEventsFromLogs(
|
||||
receipt.logs,
|
||||
[
|
||||
{
|
||||
callDataHash,
|
||||
taker,
|
||||
context: wallet.address,
|
||||
caller: zeroEx.address,
|
||||
data: transformations[0].data,
|
||||
inputTokenBalance: inputTokenAmount,
|
||||
ethBalance: callValue,
|
||||
},
|
||||
{
|
||||
callDataHash,
|
||||
taker,
|
||||
context: wallet.address,
|
||||
caller: zeroEx.address,
|
||||
data: transformations[1].data,
|
||||
inputTokenBalance: inputTokenAmount.minus(1),
|
||||
ethBalance: callValue,
|
||||
},
|
||||
],
|
||||
TestMintTokenERC20TransformerEvents.MintTransform,
|
||||
);
|
||||
});
|
||||
|
||||
it('fails with third-party transformer', async () => {
|
||||
const startingOutputTokenBalance = getRandomInteger(0, '100e18');
|
||||
const startingInputTokenBalance = getRandomInteger(2, '100e18');
|
||||
await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync();
|
||||
await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync();
|
||||
const inputTokenAmount = getRandomPortion(startingInputTokenBalance);
|
||||
const minOutputTokenAmount = getRandomInteger(2, '1e18');
|
||||
const callValue = getRandomInteger(1, '1e18');
|
||||
const callDataHash = hexUtils.random();
|
||||
const transformations = [createMintTokenTransformation({ transformer: randomAddress() })];
|
||||
const tx = feature
|
||||
._transformERC20(
|
||||
callDataHash,
|
||||
taker,
|
||||
inputToken.address,
|
||||
outputToken.address,
|
||||
inputTokenAmount,
|
||||
minOutputTokenAmount,
|
||||
transformations,
|
||||
)
|
||||
.awaitTransactionSuccessAsync({ value: callValue });
|
||||
return expect(tx).to.revertWith(new ZeroExRevertErrors.TransformERC20.InvalidRLPNonceError(NULL_BYTES));
|
||||
});
|
||||
|
||||
it('fails with incorrect transformer RLP nonce', async () => {
|
||||
const startingOutputTokenBalance = getRandomInteger(0, '100e18');
|
||||
const startingInputTokenBalance = getRandomInteger(2, '100e18');
|
||||
await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync();
|
||||
await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync();
|
||||
const inputTokenAmount = getRandomPortion(startingInputTokenBalance);
|
||||
const minOutputTokenAmount = getRandomInteger(2, '1e18');
|
||||
const callValue = getRandomInteger(1, '1e18');
|
||||
const callDataHash = hexUtils.random();
|
||||
const badRlpNonce = '0x00';
|
||||
const transformations = [createMintTokenTransformation({ rlpNonce: badRlpNonce })];
|
||||
const tx = feature
|
||||
._transformERC20(
|
||||
callDataHash,
|
||||
taker,
|
||||
inputToken.address,
|
||||
outputToken.address,
|
||||
inputTokenAmount,
|
||||
minOutputTokenAmount,
|
||||
transformations,
|
||||
)
|
||||
.awaitTransactionSuccessAsync({ value: callValue });
|
||||
return expect(tx).to.revertWith(
|
||||
new ZeroExRevertErrors.TransformERC20.UnauthorizedTransformerError(
|
||||
transformations[0].transformer,
|
||||
badRlpNonce,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('fails with invalid transformer RLP nonce', async () => {
|
||||
const startingOutputTokenBalance = getRandomInteger(0, '100e18');
|
||||
const startingInputTokenBalance = getRandomInteger(2, '100e18');
|
||||
await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync();
|
||||
await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync();
|
||||
const inputTokenAmount = getRandomPortion(startingInputTokenBalance);
|
||||
const minOutputTokenAmount = getRandomInteger(2, '1e18');
|
||||
const callValue = getRandomInteger(1, '1e18');
|
||||
const callDataHash = hexUtils.random();
|
||||
const badRlpNonce = '0x010203040506';
|
||||
const transformations = [createMintTokenTransformation({ rlpNonce: badRlpNonce })];
|
||||
const tx = feature
|
||||
._transformERC20(
|
||||
callDataHash,
|
||||
taker,
|
||||
inputToken.address,
|
||||
outputToken.address,
|
||||
inputTokenAmount,
|
||||
minOutputTokenAmount,
|
||||
transformations,
|
||||
)
|
||||
.awaitTransactionSuccessAsync({ value: callValue });
|
||||
return expect(tx).to.revertWith(new ZeroExRevertErrors.TransformERC20.InvalidRLPNonceError(badRlpNonce));
|
||||
});
|
||||
});
|
||||
});
|
211
contracts/zero-ex/test/flash_wallet_test.ts
Normal file
211
contracts/zero-ex/test/flash_wallet_test.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import {
|
||||
blockchainTests,
|
||||
constants,
|
||||
expect,
|
||||
getRandomInteger,
|
||||
randomAddress,
|
||||
verifyEventsFromLogs,
|
||||
} from '@0x/contracts-test-utils';
|
||||
import { hexUtils, OwnableRevertErrors, StringRevertError, ZeroExRevertErrors } from '@0x/utils';
|
||||
|
||||
import { artifacts } from './artifacts';
|
||||
import { FlashWalletContract, TestCallTargetContract, TestCallTargetEvents } from './wrappers';
|
||||
|
||||
blockchainTests.resets('FlashWallet', env => {
|
||||
let owner: string;
|
||||
let wallet: FlashWalletContract;
|
||||
let callTarget: TestCallTargetContract;
|
||||
|
||||
before(async () => {
|
||||
[owner] = await env.getAccountAddressesAsync();
|
||||
wallet = await FlashWalletContract.deployFrom0xArtifactAsync(
|
||||
artifacts.FlashWallet,
|
||||
env.provider,
|
||||
{
|
||||
...env.txDefaults,
|
||||
from: owner,
|
||||
},
|
||||
artifacts,
|
||||
);
|
||||
callTarget = await TestCallTargetContract.deployFrom0xArtifactAsync(
|
||||
artifacts.TestCallTarget,
|
||||
env.provider,
|
||||
env.txDefaults,
|
||||
artifacts,
|
||||
);
|
||||
});
|
||||
|
||||
const TARGET_RETURN_VALUE = hexUtils.rightPad('0x12345678');
|
||||
const REVERTING_DATA = '0x1337';
|
||||
|
||||
it('owned by deployer', () => {
|
||||
return expect(wallet.owner().callAsync()).to.eventually.eq(owner);
|
||||
});
|
||||
|
||||
describe('executeCall()', () => {
|
||||
it('non-owner cannot call executeCall()', async () => {
|
||||
const notOwner = randomAddress();
|
||||
const tx = wallet
|
||||
.executeCall(randomAddress(), hexUtils.random(), getRandomInteger(0, '100e18'))
|
||||
.callAsync({ from: notOwner });
|
||||
return expect(tx).to.revertWith(new OwnableRevertErrors.OnlyOwnerError(notOwner));
|
||||
});
|
||||
|
||||
it('owner can call executeCall()', async () => {
|
||||
const targetData = hexUtils.random(128);
|
||||
const receipt = await wallet
|
||||
.executeCall(callTarget.address, targetData, constants.ZERO_AMOUNT)
|
||||
.awaitTransactionSuccessAsync({ from: owner });
|
||||
verifyEventsFromLogs(
|
||||
receipt.logs,
|
||||
[
|
||||
{
|
||||
context: callTarget.address,
|
||||
sender: wallet.address,
|
||||
data: targetData,
|
||||
value: constants.ZERO_AMOUNT,
|
||||
},
|
||||
],
|
||||
TestCallTargetEvents.CallTargetCalled,
|
||||
);
|
||||
});
|
||||
|
||||
it('owner can call executeCall() with attached ETH', async () => {
|
||||
const targetData = hexUtils.random(128);
|
||||
const callValue = getRandomInteger(1, '1e18');
|
||||
const receipt = await wallet
|
||||
.executeCall(callTarget.address, targetData, callValue)
|
||||
.awaitTransactionSuccessAsync({ from: owner, value: callValue });
|
||||
verifyEventsFromLogs(
|
||||
receipt.logs,
|
||||
[
|
||||
{
|
||||
context: callTarget.address,
|
||||
sender: wallet.address,
|
||||
data: targetData,
|
||||
value: callValue,
|
||||
},
|
||||
],
|
||||
TestCallTargetEvents.CallTargetCalled,
|
||||
);
|
||||
});
|
||||
|
||||
it('owner can call executeCall() can transfer less ETH than attached', async () => {
|
||||
const targetData = hexUtils.random(128);
|
||||
const callValue = getRandomInteger(1, '1e18');
|
||||
const receipt = await wallet
|
||||
.executeCall(callTarget.address, targetData, callValue.minus(1))
|
||||
.awaitTransactionSuccessAsync({ from: owner, value: callValue });
|
||||
verifyEventsFromLogs(
|
||||
receipt.logs,
|
||||
[
|
||||
{
|
||||
context: callTarget.address,
|
||||
sender: wallet.address,
|
||||
data: targetData,
|
||||
value: callValue.minus(1),
|
||||
},
|
||||
],
|
||||
TestCallTargetEvents.CallTargetCalled,
|
||||
);
|
||||
});
|
||||
|
||||
it('wallet returns call result', async () => {
|
||||
const result = await wallet
|
||||
.executeCall(callTarget.address, hexUtils.random(128), constants.ZERO_AMOUNT)
|
||||
.callAsync({ from: owner });
|
||||
expect(result).to.eq(TARGET_RETURN_VALUE);
|
||||
});
|
||||
|
||||
it('wallet wraps call revert', async () => {
|
||||
const tx = wallet
|
||||
.executeCall(callTarget.address, REVERTING_DATA, constants.ZERO_AMOUNT)
|
||||
.callAsync({ from: owner });
|
||||
return expect(tx).to.revertWith(
|
||||
new ZeroExRevertErrors.Wallet.WalletExecuteCallFailedError(
|
||||
wallet.address,
|
||||
callTarget.address,
|
||||
REVERTING_DATA,
|
||||
constants.ZERO_AMOUNT,
|
||||
new StringRevertError('TestCallTarget/REVERT').encode(),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('wallet can receive ETH', async () => {
|
||||
await env.web3Wrapper.sendTransactionAsync({
|
||||
to: wallet.address,
|
||||
from: owner,
|
||||
value: 1,
|
||||
});
|
||||
const bal = await env.web3Wrapper.getBalanceInWeiAsync(wallet.address);
|
||||
expect(bal).to.bignumber.eq(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('executeDelegateCall()', () => {
|
||||
it('non-owner cannot call executeDelegateCall()', async () => {
|
||||
const notOwner = randomAddress();
|
||||
const tx = wallet.executeDelegateCall(randomAddress(), hexUtils.random()).callAsync({ from: notOwner });
|
||||
return expect(tx).to.revertWith(new OwnableRevertErrors.OnlyOwnerError(notOwner));
|
||||
});
|
||||
|
||||
it('owner can call executeDelegateCall()', async () => {
|
||||
const targetData = hexUtils.random(128);
|
||||
const receipt = await wallet
|
||||
.executeDelegateCall(callTarget.address, targetData)
|
||||
.awaitTransactionSuccessAsync({ from: owner });
|
||||
verifyEventsFromLogs(
|
||||
receipt.logs,
|
||||
[
|
||||
{
|
||||
context: wallet.address,
|
||||
sender: owner,
|
||||
data: targetData,
|
||||
value: constants.ZERO_AMOUNT,
|
||||
},
|
||||
],
|
||||
TestCallTargetEvents.CallTargetCalled,
|
||||
);
|
||||
});
|
||||
|
||||
it('executeDelegateCall() is payable', async () => {
|
||||
const targetData = hexUtils.random(128);
|
||||
const callValue = getRandomInteger(1, '1e18');
|
||||
const receipt = await wallet
|
||||
.executeDelegateCall(callTarget.address, targetData)
|
||||
.awaitTransactionSuccessAsync({ from: owner, value: callValue });
|
||||
verifyEventsFromLogs(
|
||||
receipt.logs,
|
||||
[
|
||||
{
|
||||
context: wallet.address,
|
||||
sender: owner,
|
||||
data: targetData,
|
||||
value: callValue,
|
||||
},
|
||||
],
|
||||
TestCallTargetEvents.CallTargetCalled,
|
||||
);
|
||||
});
|
||||
|
||||
it('wallet returns call result', async () => {
|
||||
const result = await wallet
|
||||
.executeDelegateCall(callTarget.address, hexUtils.random(128))
|
||||
.callAsync({ from: owner });
|
||||
expect(result).to.eq(TARGET_RETURN_VALUE);
|
||||
});
|
||||
|
||||
it('wallet wraps call revert', async () => {
|
||||
const tx = wallet.executeDelegateCall(callTarget.address, REVERTING_DATA).callAsync({ from: owner });
|
||||
return expect(tx).to.revertWith(
|
||||
new ZeroExRevertErrors.Wallet.WalletExecuteDelegateCallFailedError(
|
||||
wallet.address,
|
||||
callTarget.address,
|
||||
REVERTING_DATA,
|
||||
new StringRevertError('TestCallTarget/REVERT').encode(),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
165
contracts/zero-ex/test/full_migration_test.ts
Normal file
165
contracts/zero-ex/test/full_migration_test.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { BaseContract } from '@0x/base-contract';
|
||||
import { blockchainTests, constants, expect, randomAddress } from '@0x/contracts-test-utils';
|
||||
import { BigNumber, hexUtils, ZeroExRevertErrors } from '@0x/utils';
|
||||
import { DataItem, MethodAbi } from 'ethereum-types';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { artifacts } from './artifacts';
|
||||
import { abis } from './utils/abis';
|
||||
import { deployFullFeaturesAsync, FullFeatures, toFeatureAdddresses } from './utils/migration';
|
||||
import {
|
||||
AllowanceTargetContract,
|
||||
IOwnableContract,
|
||||
ITokenSpenderContract,
|
||||
ITransformERC20Contract,
|
||||
TestFullMigrationContract,
|
||||
ZeroExContract,
|
||||
} from './wrappers';
|
||||
|
||||
const { NULL_ADDRESS } = constants;
|
||||
|
||||
blockchainTests.resets('Full migration', env => {
|
||||
let owner: string;
|
||||
let zeroEx: ZeroExContract;
|
||||
let features: FullFeatures;
|
||||
let migrator: TestFullMigrationContract;
|
||||
|
||||
before(async () => {
|
||||
[owner] = await env.getAccountAddressesAsync();
|
||||
features = await deployFullFeaturesAsync(env.provider, env.txDefaults);
|
||||
migrator = await TestFullMigrationContract.deployFrom0xArtifactAsync(
|
||||
artifacts.TestFullMigration,
|
||||
env.provider,
|
||||
env.txDefaults,
|
||||
artifacts,
|
||||
env.txDefaults.from as string,
|
||||
);
|
||||
const deployCall = migrator.deploy(owner, toFeatureAdddresses(features));
|
||||
zeroEx = new ZeroExContract(await deployCall.callAsync(), env.provider, env.txDefaults);
|
||||
await deployCall.awaitTransactionSuccessAsync();
|
||||
});
|
||||
|
||||
it('ZeroEx has the correct owner', async () => {
|
||||
const ownable = new IOwnableContract(zeroEx.address, env.provider, env.txDefaults);
|
||||
const actualOwner = await ownable.owner().callAsync();
|
||||
expect(actualOwner).to.eq(owner);
|
||||
});
|
||||
|
||||
it('FullMigration contract self-destructs', async () => {
|
||||
const dieRecipient = await migrator.dieRecipient().callAsync();
|
||||
expect(dieRecipient).to.eq(owner);
|
||||
});
|
||||
|
||||
it('Non-deployer cannot call deploy()', async () => {
|
||||
const notDeployer = randomAddress();
|
||||
const tx = migrator.deploy(owner, toFeatureAdddresses(features)).callAsync({ from: notDeployer });
|
||||
return expect(tx).to.revertWith('FullMigration/INVALID_SENDER');
|
||||
});
|
||||
|
||||
const FEATURE_FNS = {
|
||||
TokenSpender: {
|
||||
contractType: ITokenSpenderContract,
|
||||
fns: ['_spendERC20Tokens'],
|
||||
},
|
||||
TransformERC20: {
|
||||
contractType: ITransformERC20Contract,
|
||||
fns: ['transformERC20', '_transformERC20', 'createTransformWallet', 'getTransformWallet'],
|
||||
},
|
||||
};
|
||||
|
||||
function createFakeInputs(inputs: DataItem[] | DataItem): any | any[] {
|
||||
if ((inputs as DataItem[]).length !== undefined) {
|
||||
return (inputs as DataItem[]).map(i => createFakeInputs(i));
|
||||
}
|
||||
const item = inputs as DataItem;
|
||||
// TODO(dorothy-zbornak): Support fixed-length arrays.
|
||||
if (/\[]$/.test(item.type)) {
|
||||
return _.times(_.random(0, 8), () =>
|
||||
createFakeInputs({
|
||||
...item,
|
||||
type: item.type.substring(0, item.type.length - 2),
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (/^tuple$/.test(item.type)) {
|
||||
const tuple = {} as any;
|
||||
for (const comp of item.components as DataItem[]) {
|
||||
tuple[comp.name] = createFakeInputs(comp);
|
||||
}
|
||||
return tuple;
|
||||
}
|
||||
if (item.type === 'address') {
|
||||
return randomAddress();
|
||||
}
|
||||
if (item.type === 'byte') {
|
||||
return hexUtils.random(1);
|
||||
}
|
||||
if (/^bytes$/.test(item.type)) {
|
||||
return hexUtils.random(_.random(0, 128));
|
||||
}
|
||||
if (/^bytes\d+$/.test(item.type)) {
|
||||
return hexUtils.random(parseInt(/\d+$/.exec(item.type)![0], 10));
|
||||
}
|
||||
if (/^uint\d+$/.test(item.type)) {
|
||||
return new BigNumber(hexUtils.random(parseInt(/\d+$/.exec(item.type)![0], 10) / 8));
|
||||
}
|
||||
if (/^int\d+$/.test(item.type)) {
|
||||
return new BigNumber(hexUtils.random(parseInt(/\d+$/.exec(item.type)![0], 10) / 8))
|
||||
.div(2)
|
||||
.times(_.sample([-1, 1])!);
|
||||
}
|
||||
throw new Error(`Unhandled input type: '${item.type}'`);
|
||||
}
|
||||
|
||||
for (const [featureName, featureInfo] of Object.entries(FEATURE_FNS)) {
|
||||
describe(`${featureName} feature`, () => {
|
||||
let contract: BaseContract & { getSelector(name: string): string };
|
||||
|
||||
before(async () => {
|
||||
contract = new featureInfo.contractType(zeroEx.address, env.provider, env.txDefaults, abis);
|
||||
});
|
||||
|
||||
for (const fn of featureInfo.fns) {
|
||||
it(`${fn} is registered`, async () => {
|
||||
const selector = contract.getSelector(fn);
|
||||
const impl = await zeroEx.getFunctionImplementation(selector).callAsync();
|
||||
expect(impl).to.not.eq(NULL_ADDRESS);
|
||||
});
|
||||
|
||||
if (fn.startsWith('_')) {
|
||||
it(`${fn} cannot be called from outside`, async () => {
|
||||
const method = contract.abi.find(
|
||||
d => d.type === 'function' && (d as MethodAbi).name === fn,
|
||||
) as MethodAbi;
|
||||
const inputs = createFakeInputs(method.inputs);
|
||||
const tx = (contract as any)[fn](...inputs).callAsync();
|
||||
return expect(tx).to.revertWith(
|
||||
new ZeroExRevertErrors.Common.OnlyCallableBySelfError(env.txDefaults.from),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
describe("TokenSpender's allowance target", () => {
|
||||
let allowanceTarget: AllowanceTargetContract;
|
||||
|
||||
before(async () => {
|
||||
const contract = new ITokenSpenderContract(zeroEx.address, env.provider, env.txDefaults);
|
||||
allowanceTarget = new AllowanceTargetContract(
|
||||
await contract.getAllowanceTarget().callAsync(),
|
||||
env.provider,
|
||||
env.txDefaults,
|
||||
);
|
||||
});
|
||||
|
||||
it('is owned by owner', async () => {
|
||||
return expect(allowanceTarget.owner().callAsync()).to.become(owner);
|
||||
});
|
||||
|
||||
it('Proxy is authorized', async () => {
|
||||
return expect(allowanceTarget.authorized(zeroEx.address).callAsync()).to.become(true);
|
||||
});
|
||||
});
|
||||
});
|
@@ -2,6 +2,7 @@ import { blockchainTests, expect, randomAddress } from '@0x/contracts-test-utils
|
||||
import { ZeroExRevertErrors } from '@0x/utils';
|
||||
|
||||
import { artifacts } from './artifacts';
|
||||
import { BootstrapFeatures, deployBootstrapFeaturesAsync, toFeatureAdddresses } from './utils/migration';
|
||||
import {
|
||||
IBootstrapContract,
|
||||
InitialMigrationContract,
|
||||
@@ -15,9 +16,11 @@ blockchainTests.resets('Initial migration', env => {
|
||||
let zeroEx: ZeroExContract;
|
||||
let migrator: TestInitialMigrationContract;
|
||||
let bootstrapFeature: IBootstrapContract;
|
||||
let features: BootstrapFeatures;
|
||||
|
||||
before(async () => {
|
||||
[owner] = await env.getAccountAddressesAsync();
|
||||
features = await deployBootstrapFeaturesAsync(env.provider, env.txDefaults);
|
||||
migrator = await TestInitialMigrationContract.deployFrom0xArtifactAsync(
|
||||
artifacts.TestInitialMigration,
|
||||
env.provider,
|
||||
@@ -31,7 +34,7 @@ blockchainTests.resets('Initial migration', env => {
|
||||
env.txDefaults,
|
||||
{},
|
||||
);
|
||||
const deployCall = migrator.deploy(owner);
|
||||
const deployCall = migrator.deploy(owner, toFeatureAdddresses(features));
|
||||
zeroEx = new ZeroExContract(await deployCall.callAsync(), env.provider, env.txDefaults);
|
||||
await deployCall.awaitTransactionSuccessAsync();
|
||||
});
|
||||
@@ -43,7 +46,7 @@ blockchainTests.resets('Initial migration', env => {
|
||||
|
||||
it('Non-deployer cannot call deploy()', async () => {
|
||||
const notDeployer = randomAddress();
|
||||
const tx = migrator.deploy(owner).callAsync({ from: notDeployer });
|
||||
const tx = migrator.deploy(owner, toFeatureAdddresses(features)).callAsync({ from: notDeployer });
|
||||
return expect(tx).to.revertWith('InitialMigration/INVALID_SENDER');
|
||||
});
|
||||
|
||||
@@ -67,8 +70,8 @@ blockchainTests.resets('Initial migration', env => {
|
||||
});
|
||||
|
||||
it('Bootstrap feature self destructs after deployment', async () => {
|
||||
const codeSize = await migrator.getCodeSizeOf(bootstrapFeature.address).callAsync();
|
||||
expect(codeSize).to.bignumber.eq(0);
|
||||
const doesExist = await env.web3Wrapper.doesContractExistAtAddressAsync(bootstrapFeature.address);
|
||||
expect(doesExist).to.eq(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
5
contracts/zero-ex/test/utils/abis.ts
Normal file
5
contracts/zero-ex/test/utils/abis.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { artifacts } from '../artifacts';
|
||||
|
||||
export const abis = _.mapValues(artifacts, v => v.compilerOutput.abi);
|
@@ -1,15 +1,53 @@
|
||||
import { BaseContract } from '@0x/base-contract';
|
||||
import { SupportedProvider } from '@0x/subproviders';
|
||||
import { TxData } from 'ethereum-types';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { artifacts } from '../artifacts';
|
||||
import { InitialMigrationContract, ZeroExContract } from '../wrappers';
|
||||
import {
|
||||
FullMigrationContract,
|
||||
InitialMigrationContract,
|
||||
OwnableContract,
|
||||
SimpleFunctionRegistryContract,
|
||||
TokenSpenderContract,
|
||||
TransformERC20Contract,
|
||||
ZeroExContract,
|
||||
} from '../wrappers';
|
||||
|
||||
// tslint:disable: completed-docs
|
||||
|
||||
export interface BootstrapFeatures {
|
||||
registry: SimpleFunctionRegistryContract;
|
||||
ownable: OwnableContract;
|
||||
}
|
||||
|
||||
export async function deployBootstrapFeaturesAsync(
|
||||
provider: SupportedProvider,
|
||||
txDefaults: Partial<TxData>,
|
||||
features: Partial<BootstrapFeatures> = {},
|
||||
): Promise<BootstrapFeatures> {
|
||||
return {
|
||||
registry:
|
||||
features.registry ||
|
||||
(await SimpleFunctionRegistryContract.deployFrom0xArtifactAsync(
|
||||
artifacts.SimpleFunctionRegistry,
|
||||
provider,
|
||||
txDefaults,
|
||||
artifacts,
|
||||
)),
|
||||
ownable:
|
||||
features.ownable ||
|
||||
(await OwnableContract.deployFrom0xArtifactAsync(artifacts.Ownable, provider, txDefaults, artifacts)),
|
||||
};
|
||||
}
|
||||
|
||||
export async function initialMigrateAsync(
|
||||
owner: string,
|
||||
provider: SupportedProvider,
|
||||
txDefaults: Partial<TxData>,
|
||||
features: Partial<BootstrapFeatures> = {},
|
||||
): Promise<ZeroExContract> {
|
||||
const _features = await deployBootstrapFeaturesAsync(provider, txDefaults, features);
|
||||
const migrator = await InitialMigrationContract.deployFrom0xArtifactAsync(
|
||||
artifacts.InitialMigration,
|
||||
provider,
|
||||
@@ -17,8 +55,74 @@ export async function initialMigrateAsync(
|
||||
artifacts,
|
||||
txDefaults.from as string,
|
||||
);
|
||||
const deployCall = migrator.deploy(owner);
|
||||
const deployCall = migrator.deploy(owner, toFeatureAdddresses(_features));
|
||||
const zeroEx = new ZeroExContract(await deployCall.callAsync(), provider, {});
|
||||
await deployCall.awaitTransactionSuccessAsync();
|
||||
return zeroEx;
|
||||
}
|
||||
|
||||
export interface FullFeatures extends BootstrapFeatures {
|
||||
tokenSpender: TokenSpenderContract;
|
||||
transformERC20: TransformERC20Contract;
|
||||
}
|
||||
|
||||
export interface FullMigrationOpts {
|
||||
transformDeployer: string;
|
||||
}
|
||||
|
||||
export async function deployFullFeaturesAsync(
|
||||
provider: SupportedProvider,
|
||||
txDefaults: Partial<TxData>,
|
||||
features: Partial<FullFeatures> = {},
|
||||
opts: Partial<FullMigrationOpts> = {},
|
||||
): Promise<FullFeatures> {
|
||||
return {
|
||||
...(await deployBootstrapFeaturesAsync(provider, txDefaults)),
|
||||
tokenSpender:
|
||||
features.tokenSpender ||
|
||||
(await TokenSpenderContract.deployFrom0xArtifactAsync(
|
||||
artifacts.TokenSpender,
|
||||
provider,
|
||||
txDefaults,
|
||||
artifacts,
|
||||
)),
|
||||
transformERC20:
|
||||
features.transformERC20 ||
|
||||
(await TransformERC20Contract.deployFrom0xArtifactAsync(
|
||||
artifacts.TransformERC20,
|
||||
provider,
|
||||
txDefaults,
|
||||
artifacts,
|
||||
opts.transformDeployer || (txDefaults.from as string),
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
export async function fullMigrateAsync(
|
||||
owner: string,
|
||||
provider: SupportedProvider,
|
||||
txDefaults: Partial<TxData>,
|
||||
features: Partial<FullFeatures> = {},
|
||||
opts: Partial<FullMigrationOpts> = {},
|
||||
): Promise<ZeroExContract> {
|
||||
const _features = await deployFullFeaturesAsync(provider, txDefaults, features, opts);
|
||||
const migrator = await FullMigrationContract.deployFrom0xArtifactAsync(
|
||||
artifacts.FullMigration,
|
||||
provider,
|
||||
txDefaults,
|
||||
artifacts,
|
||||
txDefaults.from as string,
|
||||
);
|
||||
const deployCall = migrator.deploy(owner, toFeatureAdddresses(_features));
|
||||
const zeroEx = new ZeroExContract(await deployCall.callAsync(), provider, {});
|
||||
await deployCall.awaitTransactionSuccessAsync();
|
||||
return zeroEx;
|
||||
}
|
||||
|
||||
// tslint:disable:space-before-function-parent one-line
|
||||
export function toFeatureAdddresses<T extends BootstrapFeatures | FullFeatures | (BootstrapFeatures & FullFeatures)>(
|
||||
features: T,
|
||||
): { [name in keyof T]: string } {
|
||||
// TS can't figure this out.
|
||||
return _.mapValues(features, (c: BaseContract) => c.address) as any;
|
||||
}
|
||||
|
@@ -3,16 +3,25 @@
|
||||
* Warning: This file is auto-generated by contracts-gen. Don't edit manually.
|
||||
* -----------------------------------------------------------------------------
|
||||
*/
|
||||
export * from '../test/generated-wrappers/allowance_target';
|
||||
export * from '../test/generated-wrappers/bootstrap';
|
||||
export * from '../test/generated-wrappers/fixin_common';
|
||||
export * from '../test/generated-wrappers/flash_wallet';
|
||||
export * from '../test/generated-wrappers/full_migration';
|
||||
export * from '../test/generated-wrappers/i_allowance_target';
|
||||
export * from '../test/generated-wrappers/i_bootstrap';
|
||||
export * from '../test/generated-wrappers/i_erc20_transformer';
|
||||
export * from '../test/generated-wrappers/i_feature';
|
||||
export * from '../test/generated-wrappers/i_flash_wallet';
|
||||
export * from '../test/generated-wrappers/i_ownable';
|
||||
export * from '../test/generated-wrappers/i_simple_function_registry';
|
||||
export * from '../test/generated-wrappers/i_test_simple_function_registry_feature';
|
||||
export * from '../test/generated-wrappers/i_token_spender';
|
||||
export * from '../test/generated-wrappers/i_transform_erc20';
|
||||
export * from '../test/generated-wrappers/initial_migration';
|
||||
export * from '../test/generated-wrappers/lib_bootstrap';
|
||||
export * from '../test/generated-wrappers/lib_common_rich_errors';
|
||||
export * from '../test/generated-wrappers/lib_erc20_transformer';
|
||||
export * from '../test/generated-wrappers/lib_migrate';
|
||||
export * from '../test/generated-wrappers/lib_ownable_rich_errors';
|
||||
export * from '../test/generated-wrappers/lib_ownable_storage';
|
||||
@@ -20,12 +29,26 @@ export * from '../test/generated-wrappers/lib_proxy_rich_errors';
|
||||
export * from '../test/generated-wrappers/lib_proxy_storage';
|
||||
export * from '../test/generated-wrappers/lib_simple_function_registry_rich_errors';
|
||||
export * from '../test/generated-wrappers/lib_simple_function_registry_storage';
|
||||
export * from '../test/generated-wrappers/lib_spender_rich_errors';
|
||||
export * from '../test/generated-wrappers/lib_storage';
|
||||
export * from '../test/generated-wrappers/lib_token_spender_storage';
|
||||
export * from '../test/generated-wrappers/lib_transform_erc20_rich_errors';
|
||||
export * from '../test/generated-wrappers/lib_transform_erc20_storage';
|
||||
export * from '../test/generated-wrappers/lib_wallet_rich_errors';
|
||||
export * from '../test/generated-wrappers/ownable';
|
||||
export * from '../test/generated-wrappers/simple_function_registry';
|
||||
export * from '../test/generated-wrappers/test_call_target';
|
||||
export * from '../test/generated-wrappers/test_full_migration';
|
||||
export * from '../test/generated-wrappers/test_initial_migration';
|
||||
export * from '../test/generated-wrappers/test_migrator';
|
||||
export * from '../test/generated-wrappers/test_mint_token_erc20_transformer';
|
||||
export * from '../test/generated-wrappers/test_mintable_erc20_token';
|
||||
export * from '../test/generated-wrappers/test_simple_function_registry_feature_impl1';
|
||||
export * from '../test/generated-wrappers/test_simple_function_registry_feature_impl2';
|
||||
export * from '../test/generated-wrappers/test_token_spender';
|
||||
export * from '../test/generated-wrappers/test_token_spender_erc20_token';
|
||||
export * from '../test/generated-wrappers/test_transform_erc20';
|
||||
export * from '../test/generated-wrappers/test_zero_ex_feature';
|
||||
export * from '../test/generated-wrappers/token_spender';
|
||||
export * from '../test/generated-wrappers/transform_erc20';
|
||||
export * from '../test/generated-wrappers/zero_ex';
|
||||
|
Reference in New Issue
Block a user