diff --git a/contracts/zero-ex/contracts/src/errors/LibTransformERC20RichErrors.sol b/contracts/zero-ex/contracts/src/errors/LibTransformERC20RichErrors.sol index ea6789ea15..0513a86f65 100644 --- a/contracts/zero-ex/contracts/src/errors/LibTransformERC20RichErrors.sol +++ b/contracts/zero-ex/contracts/src/errors/LibTransformERC20RichErrors.sol @@ -230,5 +230,4 @@ library LibTransformERC20RichErrors { token ); } - } diff --git a/contracts/zero-ex/contracts/src/transformers/AffiliateFeeTransformer.sol b/contracts/zero-ex/contracts/src/transformers/AffiliateFeeTransformer.sol new file mode 100644 index 0000000000..17025ef0b2 --- /dev/null +++ b/contracts/zero-ex/contracts/src/transformers/AffiliateFeeTransformer.sol @@ -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; + } +} diff --git a/contracts/zero-ex/package.json b/contracts/zero-ex/package.json index febccd8d1c..db3c775e19 100644 --- a/contracts/zero-ex/package.json +++ b/contracts/zero-ex/package.json @@ -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", diff --git a/contracts/zero-ex/src/transformer_data_encoders.ts b/contracts/zero-ex/src/transformer_data_encoders.ts index d9da6a0e7c..26c29abff3 100644 --- a/contracts/zero-ex/src/transformer_data_encoders.ts +++ b/contracts/zero-ex/src/transformer_data_encoders.ts @@ -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); +} diff --git a/contracts/zero-ex/test/artifacts.ts b/contracts/zero-ex/test/artifacts.ts index 0d76a254d4..e1fb0476b2 100644 --- a/contracts/zero-ex/test/artifacts.ts +++ b/contracts/zero-ex/test/artifacts.ts @@ -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, diff --git a/contracts/zero-ex/test/transformers/affiliate_fee_transformer_test.ts b/contracts/zero-ex/test/transformers/affiliate_fee_transformer_test.ts new file mode 100644 index 0000000000..a88eca5486 --- /dev/null +++ b/contracts/zero-ex/test/transformers/affiliate_fee_transformer_test.ts @@ -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 { + return { + ethBalance: await env.web3Wrapper.getBalanceInWeiAsync(owner), + tokenBalance: await token.balanceOf(owner).callAsync(), + }; + } + + async function mintHostTokensAsync(amount: BigNumber): Promise { + await token.mint(host.address, amount).awaitTransactionSuccessAsync(); + } + + async function sendEtherAsync(to: string, amount: BigNumber): Promise { + 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), + }); + }); +}); diff --git a/contracts/zero-ex/test/wrappers.ts b/contracts/zero-ex/test/wrappers.ts index 9df36914d7..00bdcffb29 100644 --- a/contracts/zero-ex/test/wrappers.ts +++ b/contracts/zero-ex/test/wrappers.ts @@ -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'; diff --git a/contracts/zero-ex/tsconfig.json b/contracts/zero-ex/tsconfig.json index 7bc3fb6e6a..9c690c0edb 100644 --- a/contracts/zero-ex/tsconfig.json +++ b/contracts/zero-ex/tsconfig.json @@ -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",