add tests for LiquidityProviderFeature

This commit is contained in:
Michael Zhu 2020-09-09 18:39:58 -07:00
parent c6d738ed0c
commit 1c15ecacb0
9 changed files with 347 additions and 42 deletions

View File

@ -28,30 +28,12 @@ import "../errors/LibLiquidityProviderRichErrors.sol";
import "../fixins/FixinCommon.sol";
import "../migrations/LibMigrate.sol";
import "../storage/LibLiquidityProviderStorage.sol";
import "../vendor/v3/IERC20Bridge.sol";
import "./IFeature.sol";
import "./ILiquidityProviderFeature.sol";
import "./ITokenSpenderFeature.sol";
interface IERC20Bridge {
/// @dev Transfers `amount` of the ERC20 `tokenAddress` from `from` to `to`.
/// @param tokenAddress The address of the ERC20 token to transfer.
/// @param from Address to transfer asset from.
/// @param to Address to transfer asset to.
/// @param amount Amount of asset to transfer.
/// @param bridgeData Arbitrary asset data needed by the bridge contract.
/// @return success The magic bytes `0xdc1600f3` if successful.
function bridgeTransferFrom(
address tokenAddress,
address from,
address to,
uint256 amount,
bytes calldata bridgeData
)
external
returns (bytes4 success);
}
contract LiquidityProviderFeature is
IFeature,
ILiquidityProviderFeature,

View File

@ -0,0 +1,69 @@
/*
Copyright 2020 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.6.5;
pragma experimental ABIEncoderV2;
import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol";
import "../src/vendor/v3/IERC20Bridge.sol";
contract TestBridge is
IERC20Bridge
{
IERC20TokenV06 public immutable xAsset;
IERC20TokenV06 public immutable yAsset;
constructor(IERC20TokenV06 xAsset_, IERC20TokenV06 yAsset_)
public
{
xAsset = xAsset_;
yAsset = yAsset_;
}
/// @dev Transfers `amount` of the ERC20 `tokenAddress` from `from` to `to`.
/// @param tokenAddress The address of the ERC20 token to transfer.
/// @param from Address to transfer asset from.
/// @param to Address to transfer asset to.
/// @param amount Amount of asset to transfer.
/// @param bridgeData Arbitrary asset data needed by the bridge contract.
/// @return success The magic bytes `0xdc1600f3` if successful.
function bridgeTransferFrom(
address tokenAddress,
address from,
address to,
uint256 amount,
bytes calldata bridgeData
)
external
override
returns (bytes4 success)
{
IERC20TokenV06 takerToken = tokenAddress == address(xAsset) ? yAsset : xAsset;
uint256 takerTokenBalance = takerToken.balanceOf(address(this));
emit ERC20BridgeTransfer(
address(takerToken),
tokenAddress,
takerTokenBalance,
amount,
from,
to
);
return 0xdecaf000;
}
}

View File

@ -41,7 +41,7 @@
"config": {
"publicInterfaceContracts": "IZeroEx,ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnableFeature,ISimpleFunctionRegistryFeature,ITokenSpenderFeature,ITransformERC20Feature,FillQuoteTransformer,PayTakerTransformer,WethTransformer,OwnableFeature,SimpleFunctionRegistryFeature,TransformERC20Feature,TokenSpenderFeature,AffiliateFeeTransformer,SignatureValidatorFeature,MetaTransactionsFeature,LogMetadataTransformer,BridgeAdapter,LiquidityProviderFeature",
"abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.",
"abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|BootstrapFeature|BridgeAdapter|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinReentrancyGuard|FlashWallet|FullMigration|IAllowanceTarget|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IExchange|IFeature|IFlashWallet|IGasToken|ILiquidityProviderFeature|IMetaTransactionsFeature|IOwnableFeature|ISignatureValidatorFeature|ISimpleFunctionRegistryFeature|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibLiquidityProviderRichErrors|LibLiquidityProviderStorage|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignatureRichErrors|LibSignedCallData|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LiquidityProviderFeature|LogMetadataTransformer|MetaTransactionsFeature|MixinAdapterAddresses|MixinBalancer|MixinCurve|MixinKyber|MixinMStable|MixinMooniswap|MixinOasis|MixinUniswap|MixinUniswapV2|MixinZeroExBridge|OwnableFeature|PayTakerTransformer|SignatureValidatorFeature|SimpleFunctionRegistryFeature|TestCallTarget|TestDelegateCaller|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFullMigration|TestInitialMigration|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpenderFeature|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|WethTransformer|ZeroEx).json"
"abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|BootstrapFeature|BridgeAdapter|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinReentrancyGuard|FlashWallet|FullMigration|IAllowanceTarget|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IExchange|IFeature|IFlashWallet|IGasToken|ILiquidityProviderFeature|IMetaTransactionsFeature|IOwnableFeature|ISignatureValidatorFeature|ISimpleFunctionRegistryFeature|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibLiquidityProviderRichErrors|LibLiquidityProviderStorage|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignatureRichErrors|LibSignedCallData|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LiquidityProviderFeature|LogMetadataTransformer|MetaTransactionsFeature|MixinAdapterAddresses|MixinBalancer|MixinCurve|MixinKyber|MixinMStable|MixinMooniswap|MixinOasis|MixinUniswap|MixinUniswapV2|MixinZeroExBridge|OwnableFeature|PayTakerTransformer|SignatureValidatorFeature|SimpleFunctionRegistryFeature|TestBridge|TestCallTarget|TestDelegateCaller|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFullMigration|TestInitialMigration|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpenderFeature|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|WethTransformer|ZeroEx).json"
},
"repository": {
"type": "git",

View File

@ -75,6 +75,7 @@ import * as OwnableFeature from '../test/generated-artifacts/OwnableFeature.json
import * as PayTakerTransformer from '../test/generated-artifacts/PayTakerTransformer.json';
import * as SignatureValidatorFeature from '../test/generated-artifacts/SignatureValidatorFeature.json';
import * as SimpleFunctionRegistryFeature from '../test/generated-artifacts/SimpleFunctionRegistryFeature.json';
import * as TestBridge from '../test/generated-artifacts/TestBridge.json';
import * as TestCallTarget from '../test/generated-artifacts/TestCallTarget.json';
import * as TestDelegateCaller from '../test/generated-artifacts/TestDelegateCaller.json';
import * as TestFillQuoteTransformerBridge from '../test/generated-artifacts/TestFillQuoteTransformerBridge.json';
@ -182,6 +183,7 @@ export const artifacts = {
IExchange: IExchange as ContractArtifact,
IGasToken: IGasToken as ContractArtifact,
ITestSimpleFunctionRegistryFeature: ITestSimpleFunctionRegistryFeature as ContractArtifact,
TestBridge: TestBridge as ContractArtifact,
TestCallTarget: TestCallTarget as ContractArtifact,
TestDelegateCaller: TestDelegateCaller as ContractArtifact,
TestFillQuoteTransformerBridge: TestFillQuoteTransformerBridge as ContractArtifact,

View File

@ -1,26 +1,28 @@
import {
blockchainTests,
expect,
getRandomInteger,
randomAddress,
verifyEventsFromLogs,
} from '@0x/contracts-test-utils';
import { BigNumber, hexUtils, StringRevertError, ZeroExRevertErrors } from '@0x/utils';
import { artifacts as erc20Artifacts, DummyERC20TokenContract } from '@0x/contracts-erc20';
import { blockchainTests, constants, expect, randomAddress, verifyEventsFromLogs } from '@0x/contracts-test-utils';
import { BigNumber, OwnableRevertErrors, ZeroExRevertErrors } from '@0x/utils';
import { IZeroExContract, TokenSpenderFeatureContract } from '../../src/wrappers';
import {
IOwnableFeatureContract,
IZeroExContract,
LiquidityProviderFeatureContract,
TokenSpenderFeatureContract,
} from '../../src/wrappers';
import { artifacts } from '../artifacts';
import { abis } from '../utils/abis';
import { fullMigrateAsync } from '../utils/migration';
import { TestTokenSpenderERC20TokenContract, TestTokenSpenderERC20TokenEvents } from '../wrappers';
import { IERC20BridgeEvents, TestBridgeContract, TestWethContract } from '../wrappers';
blockchainTests.resets('LiquidityProvider feature', env => {
blockchainTests('LiquidityProvider feature', env => {
let zeroEx: IZeroExContract;
let feature: TokenSpenderFeatureContract;
let token: TestTokenSpenderERC20TokenContract;
let allowanceTarget: string;
let feature: LiquidityProviderFeatureContract;
let token: DummyERC20TokenContract;
let weth: TestWethContract;
let owner: string;
let taker: string;
before(async () => {
const [owner] = await env.getAccountAddressesAsync();
[owner, taker] = await env.getAccountAddressesAsync();
zeroEx = await fullMigrateAsync(owner, env.provider, env.txDefaults, {
tokenSpender: (await TokenSpenderFeatureContract.deployFrom0xArtifactAsync(
artifacts.TestTokenSpender,
@ -29,20 +31,220 @@ blockchainTests.resets('LiquidityProvider feature', env => {
artifacts,
)).address,
});
feature = new TokenSpenderFeatureContract(zeroEx.address, env.provider, env.txDefaults, abis);
token = await TestTokenSpenderERC20TokenContract.deployFrom0xArtifactAsync(
artifacts.TestTokenSpenderERC20Token,
const tokenSpender = new TokenSpenderFeatureContract(zeroEx.address, env.provider, env.txDefaults, abis);
const allowanceTarget = await tokenSpender.getAllowanceTarget().callAsync();
token = await DummyERC20TokenContract.deployFrom0xArtifactAsync(
erc20Artifacts.DummyERC20Token,
env.provider,
env.txDefaults,
erc20Artifacts,
constants.DUMMY_TOKEN_NAME,
constants.DUMMY_TOKEN_SYMBOL,
constants.DUMMY_TOKEN_DECIMALS,
constants.DUMMY_TOKEN_TOTAL_SUPPLY,
);
await token.setBalance(taker, constants.INITIAL_ERC20_BALANCE).awaitTransactionSuccessAsync();
weth = await TestWethContract.deployFrom0xArtifactAsync(
artifacts.TestWeth,
env.provider,
env.txDefaults,
artifacts,
);
allowanceTarget = await feature.getAllowanceTarget().callAsync();
});
await token
.approve(allowanceTarget, constants.INITIAL_ERC20_ALLOWANCE)
.awaitTransactionSuccessAsync({ from: taker });
feature = new LiquidityProviderFeatureContract(zeroEx.address, env.provider, env.txDefaults, abis);
const featureImpl = await LiquidityProviderFeatureContract.deployFrom0xArtifactAsync(
artifacts.LiquidityProviderFeature,
env.provider,
env.txDefaults,
artifacts,
weth.address,
);
await new IOwnableFeatureContract(zeroEx.address, env.provider, env.txDefaults, abis)
.migrate(featureImpl.address, featureImpl.migrate().getABIEncodedTransactionData(), owner)
.awaitTransactionSuccessAsync();
});
describe('Registry', () => {
it('`getLiquidityProviderForMarket` reverts if address is not set', async () => {
const [xAsset, yAsset] = [randomAddress(), randomAddress()];
let tx = feature.getLiquidityProviderForMarket(xAsset, yAsset).awaitTransactionSuccessAsync();
expect(tx).to.revertWith(
new ZeroExRevertErrors.LiquidityProvider.NoLiquidityProviderForMarketError(xAsset, yAsset),
);
tx = feature.getLiquidityProviderForMarket(yAsset, xAsset).awaitTransactionSuccessAsync();
return expect(tx).to.revertWith(
new ZeroExRevertErrors.LiquidityProvider.NoLiquidityProviderForMarketError(yAsset, xAsset),
);
});
describe('Swap', () => {
it('can set/get a liquidity provider address for a given market', async () => {
const expectedAddress = randomAddress();
await feature
.setLiquidityProviderForMarket(token.address, weth.address, expectedAddress)
.awaitTransactionSuccessAsync();
let actualAddress = await feature.getLiquidityProviderForMarket(token.address, weth.address).callAsync();
expect(actualAddress).to.equal(expectedAddress);
actualAddress = await feature.getLiquidityProviderForMarket(weth.address, token.address).callAsync();
expect(actualAddress).to.equal(expectedAddress);
});
it('can update a liquidity provider address for a given market', async () => {
const expectedAddress = randomAddress();
await feature
.setLiquidityProviderForMarket(token.address, weth.address, expectedAddress)
.awaitTransactionSuccessAsync();
let actualAddress = await feature.getLiquidityProviderForMarket(token.address, weth.address).callAsync();
expect(actualAddress).to.equal(expectedAddress);
actualAddress = await feature.getLiquidityProviderForMarket(weth.address, token.address).callAsync();
expect(actualAddress).to.equal(expectedAddress);
});
it('can effectively remove a liquidity provider for a market by setting the address to 0', async () => {
await feature
.setLiquidityProviderForMarket(token.address, weth.address, constants.NULL_ADDRESS)
.awaitTransactionSuccessAsync();
const tx = feature
.getLiquidityProviderForMarket(token.address, weth.address)
.awaitTransactionSuccessAsync();
return expect(tx).to.revertWith(
new ZeroExRevertErrors.LiquidityProvider.NoLiquidityProviderForMarketError(token.address, weth.address),
);
});
it('reverts if non-owner attempts to set an address', async () => {
const tx = feature
.setLiquidityProviderForMarket(randomAddress(), randomAddress(), randomAddress())
.awaitTransactionSuccessAsync({ from: taker });
return expect(tx).to.revertWith(new OwnableRevertErrors.OnlyOwnerError(taker, owner));
});
});
blockchainTests.resets('Swap', () => {
let liquidityProvider: TestBridgeContract;
const ETH_TOKEN_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE';
before(async () => {
liquidityProvider = await TestBridgeContract.deployFrom0xArtifactAsync(
artifacts.TestBridge,
env.provider,
env.txDefaults,
artifacts,
token.address,
weth.address,
);
await feature
.setLiquidityProviderForMarket(token.address, weth.address, liquidityProvider.address)
.awaitTransactionSuccessAsync();
});
it('Cannot execute a swap for a market without a liquidity provider set', async () => {
const [xAsset, yAsset] = [randomAddress(), randomAddress()];
const tx = feature
.sellToLiquidityProvider(
xAsset,
yAsset,
constants.NULL_ADDRESS,
constants.ONE_ETHER,
constants.ZERO_AMOUNT,
)
.awaitTransactionSuccessAsync({ from: taker });
return expect(tx).to.revertWith(
new ZeroExRevertErrors.LiquidityProvider.NoLiquidityProviderForMarketError(xAsset, yAsset),
);
});
it('Successfully executes an ERC20-ERC20 swap', async () => {
const tx = await feature
.sellToLiquidityProvider(
weth.address,
token.address,
constants.NULL_ADDRESS,
constants.ONE_ETHER,
constants.ZERO_AMOUNT,
)
.awaitTransactionSuccessAsync({ from: taker });
verifyEventsFromLogs(
tx.logs,
[
{
inputToken: token.address,
outputToken: weth.address,
inputTokenAmount: constants.ONE_ETHER,
outputTokenAmount: constants.ZERO_AMOUNT,
from: constants.NULL_ADDRESS,
to: taker,
},
],
IERC20BridgeEvents.ERC20BridgeTransfer,
);
});
it('Reverts if cannot fulfill the minimum buy amount', async () => {
const minBuyAmount = new BigNumber(1);
const tx = feature
.sellToLiquidityProvider(
weth.address,
token.address,
constants.NULL_ADDRESS,
constants.ONE_ETHER,
minBuyAmount,
)
.awaitTransactionSuccessAsync({ from: taker });
return expect(tx).to.revertWith(
new ZeroExRevertErrors.LiquidityProvider.LiquidityProviderIncompleteSellError(
liquidityProvider.address,
weth.address,
token.address,
constants.ONE_ETHER,
constants.ZERO_AMOUNT,
minBuyAmount,
),
);
});
it('Successfully executes an ETH-ERC20 swap', async () => {
const tx = await feature
.sellToLiquidityProvider(
token.address,
ETH_TOKEN_ADDRESS,
constants.NULL_ADDRESS,
constants.ONE_ETHER,
constants.ZERO_AMOUNT,
)
.awaitTransactionSuccessAsync({ from: taker, value: constants.ONE_ETHER });
verifyEventsFromLogs(
tx.logs,
[
{
inputToken: weth.address,
outputToken: token.address,
inputTokenAmount: constants.ONE_ETHER,
outputTokenAmount: constants.ZERO_AMOUNT,
from: constants.NULL_ADDRESS,
to: taker,
},
],
IERC20BridgeEvents.ERC20BridgeTransfer,
);
});
it('Successfully executes an ERC20-ETH swap', async () => {
const tx = await feature
.sellToLiquidityProvider(
ETH_TOKEN_ADDRESS,
token.address,
constants.NULL_ADDRESS,
constants.ONE_ETHER,
constants.ZERO_AMOUNT,
)
.awaitTransactionSuccessAsync({ from: taker });
verifyEventsFromLogs(
tx.logs,
[
{
inputToken: token.address,
outputToken: weth.address,
inputTokenAmount: constants.ONE_ETHER,
outputTokenAmount: constants.ZERO_AMOUNT,
from: constants.NULL_ADDRESS,
to: zeroEx.address,
},
],
IERC20BridgeEvents.ERC20BridgeTransfer,
);
});
});
});

View File

@ -73,6 +73,7 @@ export * from '../test/generated-wrappers/ownable_feature';
export * from '../test/generated-wrappers/pay_taker_transformer';
export * from '../test/generated-wrappers/signature_validator_feature';
export * from '../test/generated-wrappers/simple_function_registry_feature';
export * from '../test/generated-wrappers/test_bridge';
export * from '../test/generated-wrappers/test_call_target';
export * from '../test/generated-wrappers/test_delegate_caller';
export * from '../test/generated-wrappers/test_fill_quote_transformer_bridge';

View File

@ -97,6 +97,7 @@
"test/generated-artifacts/PayTakerTransformer.json",
"test/generated-artifacts/SignatureValidatorFeature.json",
"test/generated-artifacts/SimpleFunctionRegistryFeature.json",
"test/generated-artifacts/TestBridge.json",
"test/generated-artifacts/TestCallTarget.json",
"test/generated-artifacts/TestDelegateCaller.json",
"test/generated-artifacts/TestFillQuoteTransformerBridge.json",

View File

@ -54,4 +54,5 @@ export const ZeroExRevertErrors = {
Wallet: require('./revert_errors/zero-ex/wallet_revert_errors'),
MetaTransactions: require('./revert_errors/zero-ex/meta_transaction_revert_errors'),
SignatureValidator: require('./revert_errors/zero-ex/signature_validator_revert_errors'),
LiquidityProvider: require('./revert_errors/zero-ex/liquidity_provider_revert_errors'),
};

View File

@ -0,0 +1,47 @@
import { RevertError } from '../../revert_error';
import { Numberish } from '../../types';
// tslint:disable:max-classes-per-file
export class LiquidityProviderIncompleteSellError extends RevertError {
constructor(
providerAddress?: string,
makerToken?: string,
takerToken?: string,
sellAmount?: Numberish,
boughtAmount?: Numberish,
minBuyAmount?: Numberish,
) {
super(
'LiquidityProviderIncompleteSellError',
'LiquidityProviderIncompleteSellError(address providerAddress, address makerToken, address takerToken, uint256 sellAmount, uint256 boughtAmount, uint256 minBuyAmount)',
{
providerAddress,
makerToken,
takerToken,
sellAmount,
boughtAmount,
minBuyAmount,
},
);
}
}
export class NoLiquidityProviderForMarketError extends RevertError {
constructor(xAsset?: string, yAsset?: string) {
super(
'NoLiquidityProviderForMarketError',
'NoLiquidityProviderForMarketError(address xAsset, address yAsset)',
{
xAsset,
yAsset,
},
);
}
}
const types = [LiquidityProviderIncompleteSellError, NoLiquidityProviderForMarketError];
// Register the types we've defined.
for (const type of types) {
RevertError.registerType(type);
}