Merge pull request #2594 from 0xProject/feat/contracts-zero-ex/AffiliateFeeTransformer

Exchange Proxy: AffiliateFeeTransformer
This commit is contained in:
Lawrence Forman 2020-06-08 22:24:17 -04:00 committed by GitHub
commit b39189fde3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 286 additions and 2 deletions

View File

@ -230,5 +230,4 @@ library LibTransformERC20RichErrors {
token
);
}
}

View File

@ -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;
}
}

View File

@ -40,7 +40,7 @@
"config": {
"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": "./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": {
"type": "git",

View File

@ -120,3 +120,40 @@ export interface PayTakerTransformerData {
export function encodePayTakerTransformerData(data: PayTakerTransformerData): string {
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);
}

View File

@ -5,6 +5,7 @@
*/
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 Bootstrap from '../test/generated-artifacts/Bootstrap.json';
import * as FillQuoteTransformer from '../test/generated-artifacts/FillQuoteTransformer.json';
@ -104,6 +105,7 @@ export const artifacts = {
LibStorage: LibStorage as ContractArtifact,
LibTokenSpenderStorage: LibTokenSpenderStorage as ContractArtifact,
LibTransformERC20Storage: LibTransformERC20Storage as ContractArtifact,
AffiliateFeeTransformer: AffiliateFeeTransformer as ContractArtifact,
FillQuoteTransformer: FillQuoteTransformer as ContractArtifact,
IERC20Transformer: IERC20Transformer as ContractArtifact,
LibERC20Transformer: LibERC20Transformer as ContractArtifact,

View File

@ -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),
});
});
});

View File

@ -3,6 +3,7 @@
* 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/bootstrap';
export * from '../test/generated-wrappers/fill_quote_transformer';

View File

@ -16,6 +16,7 @@
"generated-artifacts/PayTakerTransformer.json",
"generated-artifacts/WethTransformer.json",
"generated-artifacts/ZeroEx.json",
"test/generated-artifacts/AffiliateFeeTransformer.json",
"test/generated-artifacts/AllowanceTarget.json",
"test/generated-artifacts/Bootstrap.json",
"test/generated-artifacts/FillQuoteTransformer.json",