Merge pull request #2594 from 0xProject/feat/contracts-zero-ex/AffiliateFeeTransformer
Exchange Proxy: AffiliateFeeTransformer
This commit is contained in:
commit
b39189fde3
@ -230,5 +230,4 @@ library LibTransformERC20RichErrors {
|
|||||||
token
|
token
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
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-utils/contracts/src/v06/errors/LibRichErrorsV06.sol";
|
||||||
|
import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol";
|
||||||
|
import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol";
|
||||||
|
import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol";
|
||||||
|
import "../errors/LibTransformERC20RichErrors.sol";
|
||||||
|
import "./Transformer.sol";
|
||||||
|
import "./LibERC20Transformer.sol";
|
||||||
|
|
||||||
|
|
||||||
|
/// @dev A transformer that transfers tokens to arbitrary addresses.
|
||||||
|
contract AffiliateFeeTransformer is
|
||||||
|
Transformer
|
||||||
|
{
|
||||||
|
// solhint-disable no-empty-blocks
|
||||||
|
using LibRichErrorsV06 for bytes;
|
||||||
|
using LibSafeMathV06 for uint256;
|
||||||
|
using LibERC20Transformer for IERC20TokenV06;
|
||||||
|
|
||||||
|
/// @dev Information for a single fee.
|
||||||
|
struct TokenFee {
|
||||||
|
// The token to transfer to `recipient`.
|
||||||
|
IERC20TokenV06 token;
|
||||||
|
// Amount of each `token` to transfer to `recipient`.
|
||||||
|
// If `amount == uint256(-1)`, the entire balance of `token` will be
|
||||||
|
// transferred.
|
||||||
|
uint256 amount;
|
||||||
|
// Recipient of `token`.
|
||||||
|
address payable recipient;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint256 private constant MAX_UINT256 = uint256(-1);
|
||||||
|
|
||||||
|
/// @dev Create this contract.
|
||||||
|
constructor()
|
||||||
|
public
|
||||||
|
Transformer()
|
||||||
|
{}
|
||||||
|
|
||||||
|
/// @dev Transfers tokens to recipients.
|
||||||
|
/// @param data ABI-encoded `TokenFee[]`, indicating which tokens to transfer.
|
||||||
|
/// @return success The success bytes (`LibERC20Transformer.TRANSFORMER_SUCCESS`).
|
||||||
|
function transform(
|
||||||
|
bytes32, // callDataHash,
|
||||||
|
address payable, // taker,
|
||||||
|
bytes calldata data
|
||||||
|
)
|
||||||
|
external
|
||||||
|
override
|
||||||
|
returns (bytes4 success)
|
||||||
|
{
|
||||||
|
TokenFee[] memory fees = abi.decode(data, (TokenFee[]));
|
||||||
|
|
||||||
|
// Transfer tokens to recipients.
|
||||||
|
for (uint256 i = 0; i < fees.length; ++i) {
|
||||||
|
uint256 amount = fees[i].amount;
|
||||||
|
if (amount == MAX_UINT256) {
|
||||||
|
amount = LibERC20Transformer.getTokenBalanceOf(fees[i].token, address(this));
|
||||||
|
}
|
||||||
|
if (amount != 0) {
|
||||||
|
fees[i].token.transformerTransfer(fees[i].recipient, amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return LibERC20Transformer.TRANSFORMER_SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
@ -40,7 +40,7 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"publicInterfaceContracts": "ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnable,ISimpleFunctionRegistry,ITokenSpender,ITransformERC20,FillQuoteTransformer,PayTakerTransformer,WethTransformer",
|
"publicInterfaceContracts": "ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnable,ISimpleFunctionRegistry,ITokenSpender,ITransformERC20,FillQuoteTransformer,PayTakerTransformer,WethTransformer",
|
||||||
"abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.",
|
"abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.",
|
||||||
"abis": "./test/generated-artifacts/@(AllowanceTarget|Bootstrap|FillQuoteTransformer|FixinCommon|FlashWallet|FullMigration|IAllowanceTarget|IBootstrap|IERC20Transformer|IExchange|IFeature|IFlashWallet|IOwnable|ISimpleFunctionRegistry|ITestSimpleFunctionRegistryFeature|ITokenSpender|ITransformERC20|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibMigrate|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|Ownable|PayTakerTransformer|SimpleFunctionRegistry|TestCallTarget|TestDelegateCaller|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFullMigration|TestInitialMigration|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpender|TransformERC20|Transformer|TransformerDeployer|WethTransformer|ZeroEx).json"
|
"abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|Bootstrap|FillQuoteTransformer|FixinCommon|FlashWallet|FullMigration|IAllowanceTarget|IBootstrap|IERC20Transformer|IExchange|IFeature|IFlashWallet|IOwnable|ISimpleFunctionRegistry|ITestSimpleFunctionRegistryFeature|ITokenSpender|ITransformERC20|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibMigrate|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|Ownable|PayTakerTransformer|SimpleFunctionRegistry|TestCallTarget|TestDelegateCaller|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFullMigration|TestInitialMigration|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpender|TransformERC20|Transformer|TransformerDeployer|WethTransformer|ZeroEx).json"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -120,3 +120,40 @@ export interface PayTakerTransformerData {
|
|||||||
export function encodePayTakerTransformerData(data: PayTakerTransformerData): string {
|
export function encodePayTakerTransformerData(data: PayTakerTransformerData): string {
|
||||||
return payTakerTransformerDataEncoder.encode([data]);
|
return payTakerTransformerDataEncoder.encode([data]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ABI encoder for `PayTakerTransformer.TransformData`
|
||||||
|
*/
|
||||||
|
export const affiliateFeeTransformerDataEncoder = AbiEncoder.create({
|
||||||
|
name: 'data',
|
||||||
|
type: 'tuple',
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
name: 'fees',
|
||||||
|
type: 'tuple[]',
|
||||||
|
components: [
|
||||||
|
{ name: 'token', type: 'address' },
|
||||||
|
{ name: 'amount', type: 'uint256' },
|
||||||
|
{ name: 'recipient', type: 'address' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `AffiliateFeeTransformer.TransformData`
|
||||||
|
*/
|
||||||
|
export interface AffiliateFeeTransformerData {
|
||||||
|
fees: Array<{
|
||||||
|
token: string;
|
||||||
|
amount: BigNumber;
|
||||||
|
recipient: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ABI-encode a `AffiliateFeeTransformer.TransformData` type.
|
||||||
|
*/
|
||||||
|
export function encodeAffiliateFeeTransformerData(data: AffiliateFeeTransformerData): string {
|
||||||
|
return affiliateFeeTransformerDataEncoder.encode(data);
|
||||||
|
}
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
import { ContractArtifact } from 'ethereum-types';
|
import { ContractArtifact } from 'ethereum-types';
|
||||||
|
|
||||||
|
import * as AffiliateFeeTransformer from '../test/generated-artifacts/AffiliateFeeTransformer.json';
|
||||||
import * as AllowanceTarget from '../test/generated-artifacts/AllowanceTarget.json';
|
import * as AllowanceTarget from '../test/generated-artifacts/AllowanceTarget.json';
|
||||||
import * as Bootstrap from '../test/generated-artifacts/Bootstrap.json';
|
import * as Bootstrap from '../test/generated-artifacts/Bootstrap.json';
|
||||||
import * as FillQuoteTransformer from '../test/generated-artifacts/FillQuoteTransformer.json';
|
import * as FillQuoteTransformer from '../test/generated-artifacts/FillQuoteTransformer.json';
|
||||||
@ -104,6 +105,7 @@ export const artifacts = {
|
|||||||
LibStorage: LibStorage as ContractArtifact,
|
LibStorage: LibStorage as ContractArtifact,
|
||||||
LibTokenSpenderStorage: LibTokenSpenderStorage as ContractArtifact,
|
LibTokenSpenderStorage: LibTokenSpenderStorage as ContractArtifact,
|
||||||
LibTransformERC20Storage: LibTransformERC20Storage as ContractArtifact,
|
LibTransformERC20Storage: LibTransformERC20Storage as ContractArtifact,
|
||||||
|
AffiliateFeeTransformer: AffiliateFeeTransformer as ContractArtifact,
|
||||||
FillQuoteTransformer: FillQuoteTransformer as ContractArtifact,
|
FillQuoteTransformer: FillQuoteTransformer as ContractArtifact,
|
||||||
IERC20Transformer: IERC20Transformer as ContractArtifact,
|
IERC20Transformer: IERC20Transformer as ContractArtifact,
|
||||||
LibERC20Transformer: LibERC20Transformer as ContractArtifact,
|
LibERC20Transformer: LibERC20Transformer as ContractArtifact,
|
||||||
|
@ -0,0 +1,157 @@
|
|||||||
|
import { blockchainTests, constants, expect, getRandomInteger, randomAddress } from '@0x/contracts-test-utils';
|
||||||
|
import { BigNumber, hexUtils } from '@0x/utils';
|
||||||
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
|
import { ETH_TOKEN_ADDRESS } from '../../src/constants';
|
||||||
|
import { encodeAffiliateFeeTransformerData } from '../../src/transformer_data_encoders';
|
||||||
|
import { artifacts } from '../artifacts';
|
||||||
|
import {
|
||||||
|
AffiliateFeeTransformerContract,
|
||||||
|
TestMintableERC20TokenContract,
|
||||||
|
TestTransformerHostContract,
|
||||||
|
} from '../wrappers';
|
||||||
|
|
||||||
|
const { MAX_UINT256, ZERO_AMOUNT } = constants;
|
||||||
|
|
||||||
|
blockchainTests.resets('AffiliateFeeTransformer', env => {
|
||||||
|
const recipients = new Array(2).fill(0).map(() => randomAddress());
|
||||||
|
let caller: string;
|
||||||
|
let token: TestMintableERC20TokenContract;
|
||||||
|
let transformer: AffiliateFeeTransformerContract;
|
||||||
|
let host: TestTransformerHostContract;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
[caller] = await env.getAccountAddressesAsync();
|
||||||
|
token = await TestMintableERC20TokenContract.deployFrom0xArtifactAsync(
|
||||||
|
artifacts.TestMintableERC20Token,
|
||||||
|
env.provider,
|
||||||
|
env.txDefaults,
|
||||||
|
artifacts,
|
||||||
|
);
|
||||||
|
transformer = await AffiliateFeeTransformerContract.deployFrom0xArtifactAsync(
|
||||||
|
artifacts.AffiliateFeeTransformer,
|
||||||
|
env.provider,
|
||||||
|
env.txDefaults,
|
||||||
|
artifacts,
|
||||||
|
);
|
||||||
|
host = await TestTransformerHostContract.deployFrom0xArtifactAsync(
|
||||||
|
artifacts.TestTransformerHost,
|
||||||
|
env.provider,
|
||||||
|
{ ...env.txDefaults, from: caller },
|
||||||
|
artifacts,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Balances {
|
||||||
|
ethBalance: BigNumber;
|
||||||
|
tokenBalance: BigNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ZERO_BALANCES = {
|
||||||
|
ethBalance: ZERO_AMOUNT,
|
||||||
|
tokenBalance: ZERO_AMOUNT,
|
||||||
|
};
|
||||||
|
|
||||||
|
async function getBalancesAsync(owner: string): Promise<Balances> {
|
||||||
|
return {
|
||||||
|
ethBalance: await env.web3Wrapper.getBalanceInWeiAsync(owner),
|
||||||
|
tokenBalance: await token.balanceOf(owner).callAsync(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function mintHostTokensAsync(amount: BigNumber): Promise<void> {
|
||||||
|
await token.mint(host.address, amount).awaitTransactionSuccessAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendEtherAsync(to: string, amount: BigNumber): Promise<void> {
|
||||||
|
await env.web3Wrapper.awaitTransactionSuccessAsync(
|
||||||
|
await env.web3Wrapper.sendTransactionAsync({
|
||||||
|
...env.txDefaults,
|
||||||
|
to,
|
||||||
|
from: caller,
|
||||||
|
value: amount,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
it('can transfer a token and ETH', async () => {
|
||||||
|
const amounts = recipients.map(() => getRandomInteger(1, '1e18'));
|
||||||
|
const tokens = [token.address, ETH_TOKEN_ADDRESS];
|
||||||
|
const data = encodeAffiliateFeeTransformerData({
|
||||||
|
fees: recipients.map((r, i) => ({
|
||||||
|
token: tokens[i],
|
||||||
|
amount: amounts[i],
|
||||||
|
recipient: r,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
await mintHostTokensAsync(amounts[0]);
|
||||||
|
await sendEtherAsync(host.address, amounts[1]);
|
||||||
|
await host
|
||||||
|
.rawExecuteTransform(transformer.address, hexUtils.random(), randomAddress(), data)
|
||||||
|
.awaitTransactionSuccessAsync();
|
||||||
|
expect(await getBalancesAsync(host.address)).to.deep.eq(ZERO_BALANCES);
|
||||||
|
expect(await getBalancesAsync(recipients[0])).to.deep.eq({
|
||||||
|
tokenBalance: amounts[0],
|
||||||
|
ethBalance: ZERO_AMOUNT,
|
||||||
|
});
|
||||||
|
expect(await getBalancesAsync(recipients[1])).to.deep.eq({
|
||||||
|
tokenBalance: ZERO_AMOUNT,
|
||||||
|
ethBalance: amounts[1],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can transfer all of a token and ETH', async () => {
|
||||||
|
const amounts = recipients.map(() => getRandomInteger(1, '1e18'));
|
||||||
|
const tokens = [token.address, ETH_TOKEN_ADDRESS];
|
||||||
|
const data = encodeAffiliateFeeTransformerData({
|
||||||
|
fees: recipients.map((r, i) => ({
|
||||||
|
token: tokens[i],
|
||||||
|
amount: MAX_UINT256,
|
||||||
|
recipient: r,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
await mintHostTokensAsync(amounts[0]);
|
||||||
|
await sendEtherAsync(host.address, amounts[1]);
|
||||||
|
await host
|
||||||
|
.rawExecuteTransform(transformer.address, hexUtils.random(), randomAddress(), data)
|
||||||
|
.awaitTransactionSuccessAsync();
|
||||||
|
expect(await getBalancesAsync(host.address)).to.deep.eq(ZERO_BALANCES);
|
||||||
|
expect(await getBalancesAsync(recipients[0])).to.deep.eq({
|
||||||
|
tokenBalance: amounts[0],
|
||||||
|
ethBalance: ZERO_AMOUNT,
|
||||||
|
});
|
||||||
|
expect(await getBalancesAsync(recipients[1])).to.deep.eq({
|
||||||
|
tokenBalance: ZERO_AMOUNT,
|
||||||
|
ethBalance: amounts[1],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can transfer less than the balance of a token and ETH', async () => {
|
||||||
|
const amounts = recipients.map(() => getRandomInteger(1, '1e18'));
|
||||||
|
const tokens = [token.address, ETH_TOKEN_ADDRESS];
|
||||||
|
const data = encodeAffiliateFeeTransformerData({
|
||||||
|
fees: recipients.map((r, i) => ({
|
||||||
|
token: tokens[i],
|
||||||
|
amount: amounts[i].minus(1),
|
||||||
|
recipient: r,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
await mintHostTokensAsync(amounts[0]);
|
||||||
|
await sendEtherAsync(host.address, amounts[1]);
|
||||||
|
await host
|
||||||
|
.rawExecuteTransform(transformer.address, hexUtils.random(), randomAddress(), data)
|
||||||
|
.awaitTransactionSuccessAsync();
|
||||||
|
expect(await getBalancesAsync(host.address)).to.deep.eq({
|
||||||
|
tokenBalance: new BigNumber(1),
|
||||||
|
ethBalance: new BigNumber(1),
|
||||||
|
});
|
||||||
|
expect(await getBalancesAsync(recipients[0])).to.deep.eq({
|
||||||
|
tokenBalance: amounts[0].minus(1),
|
||||||
|
ethBalance: ZERO_AMOUNT,
|
||||||
|
});
|
||||||
|
expect(await getBalancesAsync(recipients[1])).to.deep.eq({
|
||||||
|
tokenBalance: ZERO_AMOUNT,
|
||||||
|
ethBalance: amounts[1].minus(1),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -3,6 +3,7 @@
|
|||||||
* Warning: This file is auto-generated by contracts-gen. Don't edit manually.
|
* Warning: This file is auto-generated by contracts-gen. Don't edit manually.
|
||||||
* -----------------------------------------------------------------------------
|
* -----------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
|
export * from '../test/generated-wrappers/affiliate_fee_transformer';
|
||||||
export * from '../test/generated-wrappers/allowance_target';
|
export * from '../test/generated-wrappers/allowance_target';
|
||||||
export * from '../test/generated-wrappers/bootstrap';
|
export * from '../test/generated-wrappers/bootstrap';
|
||||||
export * from '../test/generated-wrappers/fill_quote_transformer';
|
export * from '../test/generated-wrappers/fill_quote_transformer';
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
"generated-artifacts/PayTakerTransformer.json",
|
"generated-artifacts/PayTakerTransformer.json",
|
||||||
"generated-artifacts/WethTransformer.json",
|
"generated-artifacts/WethTransformer.json",
|
||||||
"generated-artifacts/ZeroEx.json",
|
"generated-artifacts/ZeroEx.json",
|
||||||
|
"test/generated-artifacts/AffiliateFeeTransformer.json",
|
||||||
"test/generated-artifacts/AllowanceTarget.json",
|
"test/generated-artifacts/AllowanceTarget.json",
|
||||||
"test/generated-artifacts/Bootstrap.json",
|
"test/generated-artifacts/Bootstrap.json",
|
||||||
"test/generated-artifacts/FillQuoteTransformer.json",
|
"test/generated-artifacts/FillQuoteTransformer.json",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user