diff --git a/contracts/asset-proxy/CHANGELOG.json b/contracts/asset-proxy/CHANGELOG.json index 4d2c5c3dd3..067a75fa24 100644 --- a/contracts/asset-proxy/CHANGELOG.json +++ b/contracts/asset-proxy/CHANGELOG.json @@ -1,4 +1,17 @@ [ + { + "version": "3.3.0", + "changes": [ + { + "note": "Use `LibERC20Token.approveIfBelow()` in DEX bridges for for approvals.", + "pr": 2512 + }, + { + "note": "Emit `ERC20BridgeTransfer` events in bridges.", + "pr": 2512 + } + ] + }, { "timestamp": 1583220306, "version": "3.2.5", diff --git a/contracts/asset-proxy/contracts/src/bridges/CurveBridge.sol b/contracts/asset-proxy/contracts/src/bridges/CurveBridge.sol index 9bbe57f9b1..074627d5b8 100644 --- a/contracts/asset-proxy/contracts/src/bridges/CurveBridge.sol +++ b/contracts/asset-proxy/contracts/src/bridges/CurveBridge.sol @@ -35,18 +35,26 @@ contract CurveBridge is IWallet, DeploymentConstants { + struct CurveBridgeData { + address curveAddress; + int128 fromCoinIdx; + int128 toCoinIdx; + int128 version; + } + /// @dev Callback for `ICurve`. Tries to buy `amount` of /// `toTokenAddress` tokens by selling the entirety of the opposing asset /// (DAI, USDC) to the Curve contract, then transfers the bought /// tokens to `to`. /// @param toTokenAddress The token to give to `to` (i.e DAI, USDC, USDT). + /// @param from The maker (this contract). /// @param to The recipient of the bought tokens. /// @param amount Minimum amount of `toTokenAddress` tokens to buy. /// @param bridgeData The abi-encoeded "from" token address. /// @return success The magic bytes if successful. function bridgeTransferFrom( address toTokenAddress, - address /* from */, + address from, address to, uint256 amount, bytes calldata bridgeData @@ -55,32 +63,32 @@ contract CurveBridge is returns (bytes4 success) { // Decode the bridge data to get the Curve metadata. - (address curveAddress, int128 fromCoinIdx, int128 toCoinIdx, int128 version) = abi.decode(bridgeData, (address, int128, int128, int128)); - ICurve exchange = ICurve(curveAddress); + CurveBridgeData memory data = abi.decode(bridgeData, (CurveBridgeData)); - address fromTokenAddress = exchange.underlying_coins(fromCoinIdx); + address fromTokenAddress = ICurve(data.curveAddress).underlying_coins(data.fromCoinIdx); require(toTokenAddress != fromTokenAddress, "CurveBridge/INVALID_PAIR"); + uint256 fromTokenBalance = IERC20Token(fromTokenAddress).balanceOf(address(this)); // Grant an allowance to the exchange to spend `fromTokenAddress` token. - LibERC20Token.approve(fromTokenAddress, address(exchange), uint256(-1)); + LibERC20Token.approveIfBelow(fromTokenAddress, data.curveAddress, fromTokenBalance); // Try to sell all of this contract's `fromTokenAddress` token balance. - if (version == 0) { - exchange.exchange_underlying( - fromCoinIdx, - toCoinIdx, + if (data.version == 0) { + ICurve(data.curveAddress).exchange_underlying( + data.fromCoinIdx, + data.toCoinIdx, // dx - IERC20Token(fromTokenAddress).balanceOf(address(this)), + fromTokenBalance, // min dy amount, // expires block.timestamp + 1 ); } else { - exchange.exchange_underlying( - fromCoinIdx, - toCoinIdx, + ICurve(data.curveAddress).exchange_underlying( + data.fromCoinIdx, + data.toCoinIdx, // dx - IERC20Token(fromTokenAddress).balanceOf(address(this)), + fromTokenBalance, // min dy amount ); @@ -89,6 +97,15 @@ contract CurveBridge is uint256 toTokenBalance = IERC20Token(toTokenAddress).balanceOf(address(this)); // Transfer the converted `toToken`s to `to`. LibERC20Token.transfer(toTokenAddress, to, toTokenBalance); + + emit ERC20BridgeTransfer( + fromTokenAddress, + toTokenAddress, + fromTokenBalance, + toTokenBalance, + from, + to + ); return BRIDGE_SUCCESS; } diff --git a/contracts/asset-proxy/contracts/src/bridges/DydxBridge.sol b/contracts/asset-proxy/contracts/src/bridges/DydxBridge.sol index 60bcd57ead..67baa1c14c 100644 --- a/contracts/asset-proxy/contracts/src/bridges/DydxBridge.sol +++ b/contracts/asset-proxy/contracts/src/bridges/DydxBridge.sol @@ -50,7 +50,7 @@ contract DydxBridge is /// @param encodedBridgeData An abi-encoded `BridgeData` struct. /// @return success The magic bytes if successful. function bridgeTransferFrom( - address, + address, /* toTokenAddress */ address from, address to, uint256 amount, @@ -81,6 +81,7 @@ contract DydxBridge is // Run operation. This will revert on failure. IDydx(_getDydxAddress()).operate(accounts, actions); + return BRIDGE_SUCCESS; } diff --git a/contracts/asset-proxy/contracts/src/bridges/Eth2DaiBridge.sol b/contracts/asset-proxy/contracts/src/bridges/Eth2DaiBridge.sol index b3b84d8d96..6b3b18d1c8 100644 --- a/contracts/asset-proxy/contracts/src/bridges/Eth2DaiBridge.sol +++ b/contracts/asset-proxy/contracts/src/bridges/Eth2DaiBridge.sol @@ -38,13 +38,14 @@ contract Eth2DaiBridge is /// (DAI or WETH) to the Eth2Dai contract, then transfers the bought /// tokens to `to`. /// @param toTokenAddress The token to give to `to` (either DAI or WETH). + /// @param from The maker (this contract). /// @param to The recipient of the bought tokens. /// @param amount Minimum amount of `toTokenAddress` tokens to buy. /// @param bridgeData The abi-encoeded "from" token address. /// @return success The magic bytes if successful. function bridgeTransferFrom( address toTokenAddress, - address /* from */, + address from, address to, uint256 amount, bytes calldata bridgeData @@ -56,18 +57,28 @@ contract Eth2DaiBridge is (address fromTokenAddress) = abi.decode(bridgeData, (address)); IEth2Dai exchange = IEth2Dai(_getEth2DaiAddress()); + uint256 fromTokenBalance = IERC20Token(fromTokenAddress).balanceOf(address(this)); // Grant an allowance to the exchange to spend `fromTokenAddress` token. - LibERC20Token.approve(fromTokenAddress, address(exchange), uint256(-1)); + LibERC20Token.approveIfBelow(fromTokenAddress, address(exchange), fromTokenBalance); // Try to sell all of this contract's `fromTokenAddress` token balance. uint256 boughtAmount = exchange.sellAllAmount( fromTokenAddress, - IERC20Token(fromTokenAddress).balanceOf(address(this)), + fromTokenBalance, toTokenAddress, amount ); // Transfer the converted `toToken`s to `to`. LibERC20Token.transfer(toTokenAddress, to, boughtAmount); + + emit ERC20BridgeTransfer( + fromTokenAddress, + toTokenAddress, + fromTokenBalance, + boughtAmount, + from, + to + ); return BRIDGE_SUCCESS; } diff --git a/contracts/asset-proxy/contracts/src/bridges/KyberBridge.sol b/contracts/asset-proxy/contracts/src/bridges/KyberBridge.sol index 2a98663897..91cd9c547a 100644 --- a/contracts/asset-proxy/contracts/src/bridges/KyberBridge.sol +++ b/contracts/asset-proxy/contracts/src/bridges/KyberBridge.sol @@ -66,13 +66,14 @@ contract KyberBridge is /// to the `KyberNetworkProxy` contract, then transfers the bought /// tokens to `to`. /// @param toTokenAddress The token to give to `to`. + /// @param from The maker (this contract). /// @param to The recipient of the bought tokens. /// @param amount Minimum amount of `toTokenAddress` tokens to buy. /// @param bridgeData The abi-encoeded "from" token address. /// @return success The magic bytes if successful. function bridgeTransferFrom( address toTokenAddress, - address /* from */, + address from, address to, uint256 amount, bytes calldata bridgeData @@ -109,7 +110,11 @@ contract KyberBridge is } else if (state.fromTokenAddress != address(state.weth)) { // If the input token is not WETH, grant an allowance to the exchange // to spend them. - LibERC20Token.approve(state.fromTokenAddress, address(state.kyber), uint256(-1)); + LibERC20Token.approveIfBelow( + state.fromTokenAddress, + address(state.kyber), + state.fromTokenBalance + ); } else { // If the input token is WETH, unwrap it and attach it to the call. state.fromTokenAddress = KYBER_ETH_ADDRESS; @@ -143,6 +148,15 @@ contract KyberBridge is state.weth.deposit.value(boughtAmount)(); state.weth.transfer(to, boughtAmount); } + + emit ERC20BridgeTransfer( + state.fromTokenAddress == KYBER_ETH_ADDRESS ? address(state.weth) : state.fromTokenAddress, + toTokenAddress, + state.fromTokenBalance, + boughtAmount, + from, + to + ); return BRIDGE_SUCCESS; } diff --git a/contracts/asset-proxy/contracts/src/bridges/UniswapBridge.sol b/contracts/asset-proxy/contracts/src/bridges/UniswapBridge.sol index 3d0d596273..4ea7ac2d3a 100644 --- a/contracts/asset-proxy/contracts/src/bridges/UniswapBridge.sol +++ b/contracts/asset-proxy/contracts/src/bridges/UniswapBridge.sol @@ -38,10 +38,11 @@ contract UniswapBridge is { // Struct to hold `bridgeTransferFrom()` local variables in memory and to avoid // stack overflows. - struct WithdrawToState { + struct TransferState { IUniswapExchange exchange; uint256 fromTokenBalance; IEtherToken weth; + uint256 boughtAmount; } // solhint-disable no-empty-blocks @@ -55,13 +56,14 @@ contract UniswapBridge is /// `toTokenAddress` tokens by selling the entirety of the `fromTokenAddress` /// token encoded in the bridge data. /// @param toTokenAddress The token to buy and transfer to `to`. + /// @param from The maker (this contract). /// @param to The recipient of the bought tokens. /// @param amount Minimum amount of `toTokenAddress` tokens to buy. /// @param bridgeData The abi-encoded "from" token address. /// @return success The magic bytes if successful. function bridgeTransferFrom( address toTokenAddress, - address /* from */, + address from, address to, uint256 amount, bytes calldata bridgeData @@ -70,7 +72,7 @@ contract UniswapBridge is returns (bytes4 success) { // State memory object to avoid stack overflows. - WithdrawToState memory state; + TransferState memory state; // Decode the bridge data to get the `fromTokenAddress`. (address fromTokenAddress) = abi.decode(bridgeData, (address)); @@ -96,7 +98,7 @@ contract UniswapBridge is state.weth.withdraw(state.fromTokenBalance); // Buy as much of `toTokenAddress` token with ETH as possible and // transfer it to `to`. - state.exchange.ethToTokenTransferInput.value(state.fromTokenBalance)( + state.boughtAmount = state.exchange.ethToTokenTransferInput.value(state.fromTokenBalance)( // Minimum buy amount. amount, // Expires after this block. @@ -108,9 +110,9 @@ contract UniswapBridge is // Convert from a token to WETH. } else if (toTokenAddress == address(state.weth)) { // Grant the exchange an allowance. - _grantExchangeAllowance(state.exchange, fromTokenAddress); + _grantExchangeAllowance(state.exchange, fromTokenAddress, state.fromTokenBalance); // Buy as much ETH with `fromTokenAddress` token as possible. - uint256 ethBought = state.exchange.tokenToEthSwapInput( + state.boughtAmount = state.exchange.tokenToEthSwapInput( // Sell all tokens we hold. state.fromTokenBalance, // Minimum buy amount. @@ -119,17 +121,17 @@ contract UniswapBridge is block.timestamp ); // Wrap the ETH. - state.weth.deposit.value(ethBought)(); + state.weth.deposit.value(state.boughtAmount)(); // Transfer the WETH to `to`. - IEtherToken(toTokenAddress).transfer(to, ethBought); + IEtherToken(toTokenAddress).transfer(to, state.boughtAmount); // Convert from one token to another. } else { // Grant the exchange an allowance. - _grantExchangeAllowance(state.exchange, fromTokenAddress); + _grantExchangeAllowance(state.exchange, fromTokenAddress, state.fromTokenBalance); // Buy as much `toTokenAddress` token with `fromTokenAddress` token // and transfer it to `to`. - state.exchange.tokenToTokenTransferInput( + state.boughtAmount = state.exchange.tokenToTokenTransferInput( // Sell all tokens we hold. state.fromTokenBalance, // Minimum buy amount. @@ -144,6 +146,15 @@ contract UniswapBridge is toTokenAddress ); } + + emit ERC20BridgeTransfer( + fromTokenAddress, + toTokenAddress, + state.fromTokenBalance, + state.boughtAmount, + from, + to + ); return BRIDGE_SUCCESS; } @@ -165,10 +176,19 @@ contract UniswapBridge is /// on behalf of this contract. /// @param exchange The Uniswap token exchange. /// @param tokenAddress The token address for the exchange. - function _grantExchangeAllowance(IUniswapExchange exchange, address tokenAddress) + /// @param minimumAllowance The minimum necessary allowance. + function _grantExchangeAllowance( + IUniswapExchange exchange, + address tokenAddress, + uint256 minimumAllowance + ) private { - LibERC20Token.approve(tokenAddress, address(exchange), uint256(-1)); + LibERC20Token.approveIfBelow( + tokenAddress, + address(exchange), + minimumAllowance + ); } /// @dev Retrieves the uniswap exchange for a given token pair. diff --git a/contracts/asset-proxy/contracts/src/interfaces/IERC20Bridge.sol b/contracts/asset-proxy/contracts/src/interfaces/IERC20Bridge.sol index 1e5ddef19e..3f053ac9c4 100644 --- a/contracts/asset-proxy/contracts/src/interfaces/IERC20Bridge.sol +++ b/contracts/asset-proxy/contracts/src/interfaces/IERC20Bridge.sol @@ -21,7 +21,23 @@ pragma solidity ^0.5.9; contract IERC20Bridge { - // @dev Result of a successful bridge call. + /// @dev Emitted when a bridge transfer is completed. + /// @param fromToken The address of the "from" token. + /// @param toToken The address of the "to" token. + /// @param fromTokenAmount The "from" token amount consumed. + /// @param toTokenAmount The "to" token amount transferred. + /// @param from Supplier of "fromToken". + /// @param to Receiver of "toToken". + event ERC20BridgeTransfer( + address fromToken, + address toToken, + uint256 fromTokenAmount, + uint256 toTokenAmount, + address from, + address to + ); + + /// @dev Result of a successful bridge call. bytes4 constant internal BRIDGE_SUCCESS = 0xdc1600f3; /// @dev Transfers `amount` of the ERC20 `tokenAddress` from `from` to `to`. diff --git a/contracts/asset-proxy/contracts/test/TestEth2DaiBridge.sol b/contracts/asset-proxy/contracts/test/TestEth2DaiBridge.sol index 859af2c1af..ee249278b1 100644 --- a/contracts/asset-proxy/contracts/test/TestEth2DaiBridge.sol +++ b/contracts/asset-proxy/contracts/test/TestEth2DaiBridge.sol @@ -110,6 +110,10 @@ contract TestToken { return true; } + function allowance(address, address) external view returns (uint256) { + return 0; + } + /// @dev Retrieve the balance for `owner`. function balanceOf(address owner) external diff --git a/contracts/asset-proxy/contracts/test/TestKyberBridge.sol b/contracts/asset-proxy/contracts/test/TestKyberBridge.sol index 7c6e28f0e8..c8ce0b2b26 100644 --- a/contracts/asset-proxy/contracts/test/TestKyberBridge.sol +++ b/contracts/asset-proxy/contracts/test/TestKyberBridge.sol @@ -110,6 +110,10 @@ contract TestToken { return _testContract.wethDeposit.value(msg.value)(msg.sender); } + function allowance(address, address) external view returns (uint256) { + return 0; + } + function balanceOf(address owner) external view diff --git a/contracts/asset-proxy/contracts/test/TestUniswapBridge.sol b/contracts/asset-proxy/contracts/test/TestUniswapBridge.sol index f295a939f7..bc9b53b650 100644 --- a/contracts/asset-proxy/contracts/test/TestUniswapBridge.sol +++ b/contracts/asset-proxy/contracts/test/TestUniswapBridge.sol @@ -224,6 +224,10 @@ contract TestToken { TestEventsRaiser(msg.sender).raiseWethWithdraw(amount); } + function allowance(address, address) external view returns (uint256) { + return 0; + } + /// @dev Retrieve the balance for `owner`. function balanceOf(address owner) external diff --git a/contracts/erc20/CHANGELOG.json b/contracts/erc20/CHANGELOG.json index 07a60ba9a4..d33d7ae852 100644 --- a/contracts/erc20/CHANGELOG.json +++ b/contracts/erc20/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "3.2.0", + "changes": [ + { + "note": "Add `LibERC20Token.approveIfBelow()`", + "pr": 2512 + } + ] + }, { "timestamp": 1583220306, "version": "3.1.5", diff --git a/contracts/erc20/contracts/src/LibERC20Token.sol b/contracts/erc20/contracts/src/LibERC20Token.sol index 2ba86732b3..651375c62b 100644 --- a/contracts/erc20/contracts/src/LibERC20Token.sol +++ b/contracts/erc20/contracts/src/LibERC20Token.sol @@ -47,6 +47,25 @@ library LibERC20Token { _callWithOptionalBooleanResult(token, callData); } + /// @dev Calls `IERC20Token(token).approve()` and sets the allowance to the + /// maximum if the current approval is not already >= an amount. + /// Reverts if `false` is returned or if the return + /// data length is nonzero and not 32 bytes. + /// @param token The address of the token contract. + /// @param spender The address that receives an allowance. + /// @param amount The minimum allowance needed. + function approveIfBelow( + address token, + address spender, + uint256 amount + ) + internal + { + if (IERC20Token(token).allowance(address(this), spender) < amount) { + approve(token, spender, uint256(-1)); + } + } + /// @dev Calls `IERC20Token(token).transfer()`. /// Reverts if `false` is returned or if the return /// data length is nonzero and not 32 bytes. diff --git a/contracts/extensions/CHANGELOG.json b/contracts/extensions/CHANGELOG.json index c1d8456ea1..27bc24ac39 100644 --- a/contracts/extensions/CHANGELOG.json +++ b/contracts/extensions/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "6.2.0", + "changes": [ + { + "note": "Add MaximumGasPrice contract, tooling, and unit tests", + "pr": 2511 + } + ] + }, { "timestamp": 1583220306, "version": "6.1.5", diff --git a/contracts/extensions/contracts/src/MaximumGasPrice.sol b/contracts/extensions/contracts/src/MaximumGasPrice.sol new file mode 100644 index 0000000000..027d9e5d2a --- /dev/null +++ b/contracts/extensions/contracts/src/MaximumGasPrice.sol @@ -0,0 +1,50 @@ +/* + + Copyright 2019 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.5.16; + + +contract MaximumGasPrice { + // 20 Gwei + uint256 constant private DEFAULT_MAX_GAS_PRICE = 20 * (10 ** 9); + + /// @dev Checks that the current transaction's gas price is less than + /// the default maximum value of 20 Gwei. + function checkGasPrice() + external + view + { + require( + tx.gasprice <= DEFAULT_MAX_GAS_PRICE, + "MaximumGasPrice/GAS_PRICE_EXCEEDS_20_GWEI" + ); + } + + /// @dev Checks that the current transaction's gas price is less than + /// the specified maximum value. + /// @param maxGasPrice The maximum gas price allowed for the current transaction. + function checkGasPrice(uint256 maxGasPrice) + external + view + { + require( + tx.gasprice <= maxGasPrice, + "MaximumGasPrice/GAS_PRICE_EXCEEDS_MAXIMUM" + ); + } +} diff --git a/contracts/extensions/package.json b/contracts/extensions/package.json index 65ba0047ac..a77ce1b7e9 100644 --- a/contracts/extensions/package.json +++ b/contracts/extensions/package.json @@ -38,7 +38,7 @@ "docs:json": "typedoc --excludePrivate --excludeExternals --excludeProtected --ignoreCompilerErrors --target ES5 --tsconfig typedoc-tsconfig.json --json $JSON_FILE_PATH $PROJECT_FILES" }, "config": { - "abis": "./test/generated-artifacts/@(LibAssetDataTransfer|LibAssetDataTransferRichErrors|LibWethUtilsRichErrors|MixinWethUtils).json", + "abis": "./test/generated-artifacts/@(LibAssetDataTransfer|LibAssetDataTransferRichErrors|LibWethUtilsRichErrors|MaximumGasPrice|MixinWethUtils).json", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually." }, "repository": { diff --git a/contracts/extensions/src/artifacts.ts b/contracts/extensions/src/artifacts.ts index 74efc2f940..9dd9816cca 100644 --- a/contracts/extensions/src/artifacts.ts +++ b/contracts/extensions/src/artifacts.ts @@ -8,9 +8,11 @@ import { ContractArtifact } from 'ethereum-types'; import * as LibAssetDataTransfer from '../generated-artifacts/LibAssetDataTransfer.json'; import * as LibAssetDataTransferRichErrors from '../generated-artifacts/LibAssetDataTransferRichErrors.json'; import * as LibWethUtilsRichErrors from '../generated-artifacts/LibWethUtilsRichErrors.json'; +import * as MaximumGasPrice from '../generated-artifacts/MaximumGasPrice.json'; import * as MixinWethUtils from '../generated-artifacts/MixinWethUtils.json'; export const artifacts = { LibAssetDataTransfer: LibAssetDataTransfer as ContractArtifact, + MaximumGasPrice: MaximumGasPrice as ContractArtifact, MixinWethUtils: MixinWethUtils as ContractArtifact, LibAssetDataTransferRichErrors: LibAssetDataTransferRichErrors as ContractArtifact, LibWethUtilsRichErrors: LibWethUtilsRichErrors as ContractArtifact, diff --git a/contracts/extensions/src/index.ts b/contracts/extensions/src/index.ts index 8615baa7c9..1e8731cfd1 100644 --- a/contracts/extensions/src/index.ts +++ b/contracts/extensions/src/index.ts @@ -29,3 +29,4 @@ export { TupleDataItem, StateMutability, } from 'ethereum-types'; +export * from './max_gas_price_utils'; diff --git a/contracts/extensions/src/max_gas_price_utils.ts b/contracts/extensions/src/max_gas_price_utils.ts new file mode 100644 index 0000000000..7816423933 --- /dev/null +++ b/contracts/extensions/src/max_gas_price_utils.ts @@ -0,0 +1,38 @@ +import { constants } from '@0x/contracts-test-utils'; +import { assetDataUtils } from '@0x/order-utils'; +import { StaticCallAssetData } from '@0x/types'; +import { AbiEncoder, BigNumber } from '@0x/utils'; + +const customGasPriceEncoder = AbiEncoder.createMethod('checkGasPrice', [{ name: 'maxGasPrice', type: 'uint256' }]); +const defaultGasPriceEncoder = AbiEncoder.createMethod('checkGasPrice', []); + +const ONE_GWEI = new BigNumber(10 ** 9); +export const TWENTY_GWEI = ONE_GWEI.times(20); + +/** + * Encodes the given stop limit data parameters into StaticCall asset data so that it can be used + * in a 0x order. + */ +export function encodeMaxGasPriceStaticCallData(maxGasPriceContractAddress: string, maxGasPrice?: BigNumber): string { + const staticCallData = + maxGasPrice === undefined ? defaultGasPriceEncoder.encode({}) : customGasPriceEncoder.encode({ maxGasPrice }); + return assetDataUtils.encodeStaticCallAssetData( + maxGasPriceContractAddress, + staticCallData, + constants.KECCAK256_NULL, + ); +} + +/** + * Decodes the maxGasPrice StaticCall asset data. + */ +export function decodeMaxGasPriceStaticCallData(assetData: string): BigNumber { + // tslint:disable-next-line:no-unnecessary-type-assertion + const { staticCallData } = assetDataUtils.decodeAssetDataOrThrow(assetData) as StaticCallAssetData; + try { + return customGasPriceEncoder.strictDecode(staticCallData); + } catch (e) { + defaultGasPriceEncoder.strictDecode(staticCallData); + return TWENTY_GWEI; + } +} diff --git a/contracts/extensions/src/wrappers.ts b/contracts/extensions/src/wrappers.ts index 3784249d9b..b08177589c 100644 --- a/contracts/extensions/src/wrappers.ts +++ b/contracts/extensions/src/wrappers.ts @@ -6,4 +6,5 @@ export * from '../generated-wrappers/lib_asset_data_transfer'; export * from '../generated-wrappers/lib_asset_data_transfer_rich_errors'; export * from '../generated-wrappers/lib_weth_utils_rich_errors'; +export * from '../generated-wrappers/maximum_gas_price'; export * from '../generated-wrappers/mixin_weth_utils'; diff --git a/contracts/extensions/test/artifacts.ts b/contracts/extensions/test/artifacts.ts index 61561bf804..d7c29b0ff3 100644 --- a/contracts/extensions/test/artifacts.ts +++ b/contracts/extensions/test/artifacts.ts @@ -8,9 +8,11 @@ import { ContractArtifact } from 'ethereum-types'; import * as LibAssetDataTransfer from '../test/generated-artifacts/LibAssetDataTransfer.json'; import * as LibAssetDataTransferRichErrors from '../test/generated-artifacts/LibAssetDataTransferRichErrors.json'; import * as LibWethUtilsRichErrors from '../test/generated-artifacts/LibWethUtilsRichErrors.json'; +import * as MaximumGasPrice from '../test/generated-artifacts/MaximumGasPrice.json'; import * as MixinWethUtils from '../test/generated-artifacts/MixinWethUtils.json'; export const artifacts = { LibAssetDataTransfer: LibAssetDataTransfer as ContractArtifact, + MaximumGasPrice: MaximumGasPrice as ContractArtifact, MixinWethUtils: MixinWethUtils as ContractArtifact, LibAssetDataTransferRichErrors: LibAssetDataTransferRichErrors as ContractArtifact, LibWethUtilsRichErrors: LibWethUtilsRichErrors as ContractArtifact, diff --git a/contracts/extensions/test/max_gas_price_test.ts b/contracts/extensions/test/max_gas_price_test.ts new file mode 100644 index 0000000000..76ac63b7cf --- /dev/null +++ b/contracts/extensions/test/max_gas_price_test.ts @@ -0,0 +1,89 @@ +import { artifacts as assetProxyArtifacts, StaticCallProxyContract } from '@0x/contracts-asset-proxy'; +import { blockchainTests, constants, expect, getRandomInteger, randomAddress } from '@0x/contracts-test-utils'; + +import { + decodeMaxGasPriceStaticCallData, + encodeMaxGasPriceStaticCallData, + TWENTY_GWEI, +} from '../src/max_gas_price_utils'; + +import { artifacts } from './artifacts'; +import { MaximumGasPriceContract } from './wrappers'; + +blockchainTests.resets('MaximumGasPrice unit tests', env => { + let maxGasPriceContract: MaximumGasPriceContract; + let staticCallProxy: StaticCallProxyContract; + + let defaultMaxAssetData: string; + + before(async () => { + maxGasPriceContract = await MaximumGasPriceContract.deployFrom0xArtifactAsync( + artifacts.MaximumGasPrice, + env.provider, + env.txDefaults, + artifacts, + ); + staticCallProxy = await StaticCallProxyContract.deployFrom0xArtifactAsync( + assetProxyArtifacts.StaticCallProxy, + env.provider, + env.txDefaults, + assetProxyArtifacts, + ); + + defaultMaxAssetData = encodeMaxGasPriceStaticCallData(maxGasPriceContract.address); + }); + + describe('Data encoding/decoding tools', () => { + it('correctly decodes default maximum gas price', async () => { + const decoded = decodeMaxGasPriceStaticCallData(defaultMaxAssetData); + expect(decoded).to.bignumber.equal(TWENTY_GWEI); + }); + it('correctly decodes custom maximum gas price', async () => { + const customMaxGasPrice = getRandomInteger(0, constants.MAX_UINT256); + const customMaxAssetData = encodeMaxGasPriceStaticCallData(maxGasPriceContract.address, customMaxGasPrice); + const decoded = decodeMaxGasPriceStaticCallData(customMaxAssetData); + expect(decoded).to.bignumber.equal(customMaxGasPrice); + }); + }); + + describe('Contract functionality', () => { + it('does not revert if tx.gasprice < default maximum', async () => { + await staticCallProxy + .transferFrom(defaultMaxAssetData, randomAddress(), randomAddress(), constants.ZERO_AMOUNT) + .callAsync({ gasPrice: TWENTY_GWEI.minus(1) }); + }); + it('does not revert if tx.gasprice = default maximum', async () => { + await staticCallProxy + .transferFrom(defaultMaxAssetData, randomAddress(), randomAddress(), constants.ZERO_AMOUNT) + .callAsync({ gasPrice: TWENTY_GWEI }); + }); + it('reverts if tx.gasPrice > default maximum', async () => { + const tx = staticCallProxy + .transferFrom(defaultMaxAssetData, randomAddress(), randomAddress(), constants.ZERO_AMOUNT) + .callAsync({ gasPrice: TWENTY_GWEI.plus(1) }); + return expect(tx).to.revertWith('MaximumGasPrice/GAS_PRICE_EXCEEDS_20_GWEI'); + }); + it('does not revert if tx.gasprice < custom maximum', async () => { + const maxGasPrice = getRandomInteger(0, TWENTY_GWEI.times(2)); + const customMaxAssetData = encodeMaxGasPriceStaticCallData(maxGasPriceContract.address, maxGasPrice); + await staticCallProxy + .transferFrom(customMaxAssetData, randomAddress(), randomAddress(), constants.ZERO_AMOUNT) + .callAsync({ gasPrice: maxGasPrice.minus(1) }); + }); + it('does not revert if tx.gasprice = custom maximum', async () => { + const maxGasPrice = getRandomInteger(0, TWENTY_GWEI.times(2)); + const customMaxAssetData = encodeMaxGasPriceStaticCallData(maxGasPriceContract.address, maxGasPrice); + await staticCallProxy + .transferFrom(customMaxAssetData, randomAddress(), randomAddress(), constants.ZERO_AMOUNT) + .callAsync({ gasPrice: maxGasPrice }); + }); + it('reverts if tx.gasPrice > custom maximum', async () => { + const maxGasPrice = getRandomInteger(0, TWENTY_GWEI.times(2)); + const customMaxAssetData = encodeMaxGasPriceStaticCallData(maxGasPriceContract.address, maxGasPrice); + const tx = staticCallProxy + .transferFrom(customMaxAssetData, randomAddress(), randomAddress(), constants.ZERO_AMOUNT) + .callAsync({ gasPrice: maxGasPrice.plus(1) }); + return expect(tx).to.revertWith('MaximumGasPrice/GAS_PRICE_EXCEEDS_MAXIMUM'); + }); + }); +}); diff --git a/contracts/extensions/test/wrappers.ts b/contracts/extensions/test/wrappers.ts index 4c946b6949..fffb69973f 100644 --- a/contracts/extensions/test/wrappers.ts +++ b/contracts/extensions/test/wrappers.ts @@ -6,4 +6,5 @@ export * from '../test/generated-wrappers/lib_asset_data_transfer'; export * from '../test/generated-wrappers/lib_asset_data_transfer_rich_errors'; export * from '../test/generated-wrappers/lib_weth_utils_rich_errors'; +export * from '../test/generated-wrappers/maximum_gas_price'; export * from '../test/generated-wrappers/mixin_weth_utils'; diff --git a/contracts/extensions/tsconfig.json b/contracts/extensions/tsconfig.json index 0f5c6dd95c..76c8c725b3 100644 --- a/contracts/extensions/tsconfig.json +++ b/contracts/extensions/tsconfig.json @@ -6,10 +6,12 @@ "generated-artifacts/LibAssetDataTransfer.json", "generated-artifacts/LibAssetDataTransferRichErrors.json", "generated-artifacts/LibWethUtilsRichErrors.json", + "generated-artifacts/MaximumGasPrice.json", "generated-artifacts/MixinWethUtils.json", "test/generated-artifacts/LibAssetDataTransfer.json", "test/generated-artifacts/LibAssetDataTransferRichErrors.json", "test/generated-artifacts/LibWethUtilsRichErrors.json", + "test/generated-artifacts/MaximumGasPrice.json", "test/generated-artifacts/MixinWethUtils.json" ], "exclude": ["./deploy/solc/solc_bin"] diff --git a/packages/contract-addresses/CHANGELOG.json b/packages/contract-addresses/CHANGELOG.json index 77d26c7c86..577e279bb9 100644 --- a/packages/contract-addresses/CHANGELOG.json +++ b/packages/contract-addresses/CHANGELOG.json @@ -1,4 +1,17 @@ [ + { + "version": "4.10.0", + "changes": [ + { + "note": "Added MaximumGasPrice addresses", + "pr": 2511 + }, + { + "note": "Redeploy `KyberBridge`, `UniswapBridge`, `Eth2DaiBridge`, `CurveBridge`, `DydxBridge` on mainnet and kovan", + "pr": 2512 + } + ] + }, { "version": "4.9.0", "changes": [ diff --git a/packages/contract-addresses/addresses.json b/packages/contract-addresses/addresses.json index 2db6ae8dd7..4c1984db7c 100644 --- a/packages/contract-addresses/addresses.json +++ b/packages/contract-addresses/addresses.json @@ -19,16 +19,17 @@ "stakingProxy": "0xa26e80e7dea86279c6d778d702cc413e6cffa777", "devUtils": "0x74134cf88b21383713e096a5ecf59e297dc7f547", "erc20BridgeProxy": "0x8ed95d1746bf1e4dab58d8ed4724f1ef95b20db0", - "uniswapBridge": "0x533344cfdf2a3e911e2cf4c6f5ed08e791f5355f", + "uniswapBridge": "0x36691c4f426eb8f42f150ebde43069a31cb080ad", "erc20BridgeSampler": "0xb2dee8cf2a06fbf0942fda5521f890b6e9911bfe", - "kyberBridge": "0xf342f3a80fdc9b48713d58fe97e17f5cc764ee62", - "eth2DaiBridge": "0xe3379a1956f4a79f39eb2e87bb441419e167538e", + "kyberBridge": "0x1c29670f7a77f1052d30813a0a4f632c78a02610", + "eth2DaiBridge": "0x991c745401d5b5e469b8c3e2cb02c748f08754f1", "chaiBridge": "0x77c31eba23043b9a72d13470f3a3a311344d7438", - "dydxBridge": "0x55dc8f21d20d4c6ed3c82916a438a413ca68e335", + "dydxBridge": "0x871299bed3ea54577a3e3a43136f7fbb5e377114", "godsUnchainedValidator": "0x09A379Ef7218BCFD8913fAa8B281ebc5A2E0bC04", "broker": "0xd4690a51044db77D91d7Aa8f7a3a5ad5dA331Af0", "chainlinkStopLimit": "0xeb27220f95f364e1d9531992c48613f231839f53", - "curveBridge": "0xe335bdd1fb0ee30f9a9a434f18f8b118dec32df7" + "curveBridge": "0x6dc7950423ada9f56fb2c93a23edb787f1e29088", + "maximumGasPrice": "0xe2bfd35306495d11e3c9db0d8de390cda24563cf" }, "3": { "erc20Proxy": "0xb1408f4c245a23c31b98d2c626777d4c0d766caa", @@ -59,7 +60,8 @@ "godsUnchainedValidator": "0xd4690a51044db77D91d7Aa8f7a3a5ad5dA331Af0", "broker": "0x4Aa817C6f383C8e8aE77301d18Ce48efb16Fd2BE", "chainlinkStopLimit": "0x67a094cf028221ffdd93fc658f963151d05e2a74", - "curveBridge": "0x0000000000000000000000000000000000000000" + "curveBridge": "0x0000000000000000000000000000000000000000", + "maximumGasPrice": "0x407b4128e9ecad8769b2332312a9f655cb9f5f3a" }, "4": { "exchangeV2": "0xbff9493f92a3df4b0429b6d00743b3cfb4c85831", @@ -90,7 +92,8 @@ "godsUnchainedValidator": "0x0000000000000000000000000000000000000000", "broker": "0x0000000000000000000000000000000000000000", "chainlinkStopLimit": "0x407b4128e9ecad8769b2332312a9f655cb9f5f3a", - "curveBridge": "0x0000000000000000000000000000000000000000" + "curveBridge": "0x0000000000000000000000000000000000000000", + "maximumGasPrice": "0x47697b44bd89051e93b4d5857ba8e024800a74ac" }, "42": { "erc20Proxy": "0xf1ec01d6236d3cd881a0bf0130ea25fe4234003e", @@ -112,16 +115,17 @@ "staking": "0x32b06d5611a03737a5f1834a24ccd641033fd89c", "stakingProxy": "0xbab9145f1d57cd4bb0c9aa2d1ece0a5b6e734d34", "erc20BridgeProxy": "0xfb2dd2a1366de37f7241c83d47da58fd503e2c64", - "uniswapBridge": "0x8224aa8fe5c9f07d5a59c735386ff6cc6aaeb568", - "eth2DaiBridge": "0x9485d65c6a2fae0d519cced5bd830e57c41998a9", - "erc20BridgeSampler": "0xf6aeb7bc10709b06244a2c5026b8799672d40312", - "kyberBridge": "0xde7b2747624a647600fdb349184d0448ab954929", + "uniswapBridge": "0x0e85f89f29998df65402391478e5924700c0079d", + "eth2DaiBridge": "0x2d47147429b474d2e4f83e658015858a1312ed5b", + "erc20BridgeSampler": "0x4f1556e5fe03a0da39091260f78d2cf765baa091", + "kyberBridge": "0xaecfa25920f892b6eb496e1f6e84037f59da7f44", "chaiBridge": "0x0000000000000000000000000000000000000000", - "dydxBridge": "0x0000000000000000000000000000000000000000", + "dydxBridge": "0x080e183c0193b4765d504e402db2f5621d4567e4", "godsUnchainedValidator": "0x0000000000000000000000000000000000000000", "broker": "0x0000000000000000000000000000000000000000", "chainlinkStopLimit": "0x0000000000000000000000000000000000000000", - "curveBridge": "0x0000000000000000000000000000000000000000" + "curveBridge": "0x90c62c91a9f655f4f739e6cee85c84f9ccf47323", + "maximumGasPrice": "0x67a094cf028221ffdd93fc658f963151d05e2a74" }, "1337": { "erc20Proxy": "0x1dc4c1cefef38a777b15aa20260a54e584b16c48", @@ -152,6 +156,7 @@ "godsUnchainedValidator": "0x0000000000000000000000000000000000000000", "broker": "0x0000000000000000000000000000000000000000", "chainlinkStopLimit": "0x0000000000000000000000000000000000000000", - "curveBridge": "0x0000000000000000000000000000000000000000" + "curveBridge": "0x0000000000000000000000000000000000000000", + "maximumGasPrice": "0x0000000000000000000000000000000000000000" } } diff --git a/packages/contract-addresses/src/index.ts b/packages/contract-addresses/src/index.ts index 0229f9ebe2..a9d519b586 100644 --- a/packages/contract-addresses/src/index.ts +++ b/packages/contract-addresses/src/index.ts @@ -30,6 +30,7 @@ export interface ContractAddresses { godsUnchainedValidator: string; broker: string; chainlinkStopLimit: string; + maximumGasPrice: string; } export enum ChainId { diff --git a/packages/contract-artifacts/CHANGELOG.json b/packages/contract-artifacts/CHANGELOG.json index 3e957ec2b8..58e3d872c5 100644 --- a/packages/contract-artifacts/CHANGELOG.json +++ b/packages/contract-artifacts/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "3.7.0", + "changes": [ + { + "note": "Added `MaximumGasPrice` artifact", + "pr": 2511 + } + ] + }, { "timestamp": 1582623685, "version": "3.6.1", diff --git a/packages/contract-artifacts/artifacts/MaximumGasPrice.json b/packages/contract-artifacts/artifacts/MaximumGasPrice.json new file mode 100644 index 0000000000..3ccdc90cf2 --- /dev/null +++ b/packages/contract-artifacts/artifacts/MaximumGasPrice.json @@ -0,0 +1,70 @@ +{ + "schemaVersion": "2.0.0", + "contractName": "MaximumGasPrice", + "compilerOutput": { + "abi": [ + { + "constant": true, + "inputs": [], + "name": "checkGasPrice", + "outputs": [], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "internalType": "uint256", "name": "maxGasPrice", "type": "uint256" }], + "name": "checkGasPrice", + "outputs": [], + "payable": false, + "stateMutability": "view", + "type": "function" + } + ], + "devdoc": { + "methods": { + "checkGasPrice()": { + "details": "Checks that the current transaction's gas price is less than the default maximum value of 20 Gwei." + }, + "checkGasPrice(uint256)": { + "details": "Checks that the current transaction's gas price is less than the specified maximum value.", + "params": { "maxGasPrice": "The maximum gas price allowed for the current transaction." } + } + } + }, + "evm": { + "bytecode": { + "object": "0x608060405234801561001057600080fd5b506101a5806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c8063d728f5b71461003b578063da5b166a14610045575b600080fd5b610043610062565b005b6100436004803603602081101561005b57600080fd5b50356100c2565b6404a817c8003a11156100c0576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260298152602001806101486029913960400191505060405180910390fd5b565b803a111561011b576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602981526020018061011f6029913960400191505060405180910390fd5b5056fe4d6178696d756d47617350726963652f4741535f50524943455f455843454544535f4d4158494d554d4d6178696d756d47617350726963652f4741535f50524943455f455843454544535f32305f47574549a265627a7a72315820b735b9a3a2024167c985358b7c43d479b1e6d937ae2375ccb506b2092c0c20e564736f6c63430005100032" + }, + "deployedBytecode": { + "object": "0x608060405234801561001057600080fd5b50600436106100365760003560e01c8063d728f5b71461003b578063da5b166a14610045575b600080fd5b610043610062565b005b6100436004803603602081101561005b57600080fd5b50356100c2565b6404a817c8003a11156100c0576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260298152602001806101486029913960400191505060405180910390fd5b565b803a111561011b576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602981526020018061011f6029913960400191505060405180910390fd5b5056fe4d6178696d756d47617350726963652f4741535f50524943455f455843454544535f4d4158494d554d4d6178696d756d47617350726963652f4741535f50524943455f455843454544535f32305f47574549a265627a7a72315820b735b9a3a2024167c985358b7c43d479b1e6d937ae2375ccb506b2092c0c20e564736f6c63430005100032" + } + } + }, + "compiler": { + "name": "solc", + "version": "soljson-v0.5.16+commit.9c3226ce.js", + "settings": { + "optimizer": { + "enabled": true, + "runs": 1000000, + "details": { "yul": true, "deduplicate": true, "cse": true, "constantOptimizer": true } + }, + "outputSelection": { + "*": { + "*": [ + "abi", + "devdoc", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap" + ] + } + }, + "evmVersion": "istanbul" + } + }, + "chains": {} +} diff --git a/packages/contract-wrappers/CHANGELOG.json b/packages/contract-wrappers/CHANGELOG.json index d3e4f2dce9..abd74491da 100644 --- a/packages/contract-wrappers/CHANGELOG.json +++ b/packages/contract-wrappers/CHANGELOG.json @@ -1,4 +1,17 @@ [ + { + "version": "13.7.0", + "changes": [ + { + "note": "Regenerated wrappers for Broker and GodsUnchainedValidator", + "pr": 2511 + }, + { + "note": "Added wrapper for MaximumGasPrice", + "pr": 2511 + } + ] + }, { "timestamp": 1583220306, "version": "13.6.3", diff --git a/packages/contract-wrappers/package.json b/packages/contract-wrappers/package.json index 92364ec4ec..a7c0c625f8 100644 --- a/packages/contract-wrappers/package.json +++ b/packages/contract-wrappers/package.json @@ -31,7 +31,7 @@ "wrappers:generate": "abi-gen --abis ${npm_package_config_abis} --output src/generated-wrappers --backend ethers" }, "config": { - "abis": "../contract-artifacts/artifacts/@(DevUtils|ERC20Token|ERC721Token|Exchange|Forwarder|IAssetData|LibTransactionDecoder|WETH9|Coordinator|Staking|StakingProxy|IERC20BridgeSampler|ERC20BridgeSampler|GodsUnchainedValidator|Broker|ILiquidityProvider|ILiquidityProviderRegistry|DummyLiquidityProvider|DummyLiquidityProviderRegistry).json" + "abis": "../contract-artifacts/artifacts/@(DevUtils|ERC20Token|ERC721Token|Exchange|Forwarder|IAssetData|LibTransactionDecoder|WETH9|Coordinator|Staking|StakingProxy|IERC20BridgeSampler|ERC20BridgeSampler|GodsUnchainedValidator|Broker|ILiquidityProvider|ILiquidityProviderRegistry|DummyLiquidityProvider|DummyLiquidityProviderRegistry|MaximumGasPrice).json" }, "repository": { "type": "git", diff --git a/packages/contract-wrappers/src/generated-wrappers/broker.ts b/packages/contract-wrappers/src/generated-wrappers/broker.ts index 2efeafabc1..06ae217df9 100644 --- a/packages/contract-wrappers/src/generated-wrappers/broker.ts +++ b/packages/contract-wrappers/src/generated-wrappers/broker.ts @@ -9,6 +9,7 @@ import { BaseContract, PromiseWithTransactionHash, methodAbiToFunctionSignature, + linkLibrariesInBytecode, } from '@0x/base-contract'; import { schemas } from '@0x/json-schemas'; import { @@ -25,7 +26,7 @@ import { TxDataPayable, SupportedProvider, } from 'ethereum-types'; -import { BigNumber, classUtils, logUtils, providerUtils } from '@0x/utils'; +import { BigNumber, classUtils, hexUtils, logUtils, providerUtils } from '@0x/utils'; import { EventCallback, IndexedFilterValues, SimpleContractArtifact } from '@0x/types'; import { Web3Wrapper } from '@0x/web3-wrapper'; import { assert } from '@0x/assert'; @@ -33,6 +34,7 @@ import * as ethers from 'ethers'; // tslint:enable:no-unused-variable /* istanbul ignore next */ +// tslint:disable:array-type // tslint:disable:no-parameter-reassignment // tslint:disable-next-line:class-name export class BrokerContract extends BaseContract { @@ -77,6 +79,50 @@ export class BrokerContract extends BaseContract { weth, ); } + + public static async deployWithLibrariesFrom0xArtifactAsync( + artifact: ContractArtifact, + libraryArtifacts: { [libraryName: string]: ContractArtifact }, + supportedProvider: SupportedProvider, + txDefaults: Partial, + logDecodeDependencies: { [contractName: string]: ContractArtifact | SimpleContractArtifact }, + exchange: string, + weth: string, + ): Promise { + assert.doesConformToSchema('txDefaults', txDefaults, schemas.txDataSchema, [ + schemas.addressSchema, + schemas.numberSchema, + schemas.jsNumber, + ]); + if (artifact.compilerOutput === undefined) { + throw new Error('Compiler output not found in the artifact file'); + } + const provider = providerUtils.standardizeOrThrow(supportedProvider); + const abi = artifact.compilerOutput.abi; + const logDecodeDependenciesAbiOnly: { [contractName: string]: ContractAbi } = {}; + if (Object.keys(logDecodeDependencies) !== undefined) { + for (const key of Object.keys(logDecodeDependencies)) { + logDecodeDependenciesAbiOnly[key] = logDecodeDependencies[key].compilerOutput.abi; + } + } + const libraryAddresses = await BrokerContract._deployLibrariesAsync( + artifact, + libraryArtifacts, + new Web3Wrapper(provider), + txDefaults, + ); + const bytecode = linkLibrariesInBytecode(artifact, libraryAddresses); + return BrokerContract.deployAsync( + bytecode, + abi, + provider, + txDefaults, + logDecodeDependenciesAbiOnly, + exchange, + weth, + ); + } + public static async deployAsync( bytecode: string, abi: ContractAbi, @@ -432,12 +478,58 @@ export class BrokerContract extends BaseContract { return abi; } + protected static async _deployLibrariesAsync( + artifact: ContractArtifact, + libraryArtifacts: { [libraryName: string]: ContractArtifact }, + web3Wrapper: Web3Wrapper, + txDefaults: Partial, + libraryAddresses: { [libraryName: string]: string } = {}, + ): Promise<{ [libraryName: string]: string }> { + const links = artifact.compilerOutput.evm.bytecode.linkReferences; + // Go through all linked libraries, recursively deploying them if necessary. + for (const link of Object.values(links)) { + for (const libraryName of Object.keys(link)) { + if (!libraryAddresses[libraryName]) { + // Library not yet deployed. + const libraryArtifact = libraryArtifacts[libraryName]; + if (!libraryArtifact) { + throw new Error(`Missing artifact for linked library "${libraryName}"`); + } + // Deploy any dependent libraries used by this library. + await BrokerContract._deployLibrariesAsync( + libraryArtifact, + libraryArtifacts, + web3Wrapper, + txDefaults, + libraryAddresses, + ); + // Deploy this library. + const linkedLibraryBytecode = linkLibrariesInBytecode(libraryArtifact, libraryAddresses); + const txDataWithDefaults = await BaseContract._applyDefaultsToContractTxDataAsync( + { + data: linkedLibraryBytecode, + ...txDefaults, + }, + web3Wrapper.estimateGasAsync.bind(web3Wrapper), + ); + const txHash = await web3Wrapper.sendTransactionAsync(txDataWithDefaults); + logUtils.log(`transactionHash: ${txHash}`); + const { contractAddress } = await web3Wrapper.awaitTransactionSuccessAsync(txHash); + logUtils.log(`${libraryArtifact.contractName} successfully deployed at ${contractAddress}`); + libraryAddresses[libraryArtifact.contractName] = contractAddress as string; + } + } + } + return libraryAddresses; + } + public getFunctionSignature(methodName: string): string { const index = this._methodABIIndex[methodName]; const methodAbi = BrokerContract.ABI()[index] as MethodAbi; // tslint:disable-line:no-unnecessary-type-assertion const functionSignature = methodAbiToFunctionSignature(methodAbi); return functionSignature; } + public getABIDecodedTransactionData(methodName: string, callData: string): T { const functionSignature = this.getFunctionSignature(methodName); const self = (this as any) as BrokerContract; @@ -445,6 +537,7 @@ export class BrokerContract extends BaseContract { const abiDecodedCallData = abiEncoder.strictDecode(callData); return abiDecodedCallData; } + public getABIDecodedReturnData(methodName: string, callData: string): T { const functionSignature = this.getFunctionSignature(methodName); const self = (this as any) as BrokerContract; @@ -452,6 +545,7 @@ export class BrokerContract extends BaseContract { const abiDecodedCallData = abiEncoder.strictDecodeReturnValue(callData); return abiDecodedCallData; } + public getSelector(methodName: string): string { const functionSignature = this.getFunctionSignature(methodName); const self = (this as any) as BrokerContract; diff --git a/packages/contract-wrappers/src/generated-wrappers/gods_unchained_validator.ts b/packages/contract-wrappers/src/generated-wrappers/gods_unchained_validator.ts index cfacae76af..67ed233daf 100644 --- a/packages/contract-wrappers/src/generated-wrappers/gods_unchained_validator.ts +++ b/packages/contract-wrappers/src/generated-wrappers/gods_unchained_validator.ts @@ -9,6 +9,7 @@ import { BaseContract, PromiseWithTransactionHash, methodAbiToFunctionSignature, + linkLibrariesInBytecode, } from '@0x/base-contract'; import { schemas } from '@0x/json-schemas'; import { @@ -25,7 +26,7 @@ import { TxDataPayable, SupportedProvider, } from 'ethereum-types'; -import { BigNumber, classUtils, logUtils, providerUtils } from '@0x/utils'; +import { BigNumber, classUtils, hexUtils, logUtils, providerUtils } from '@0x/utils'; import { EventCallback, IndexedFilterValues, SimpleContractArtifact } from '@0x/types'; import { Web3Wrapper } from '@0x/web3-wrapper'; import { assert } from '@0x/assert'; @@ -33,6 +34,7 @@ import * as ethers from 'ethers'; // tslint:enable:no-unused-variable /* istanbul ignore next */ +// tslint:disable:array-type // tslint:disable:no-parameter-reassignment // tslint:disable-next-line:class-name export class GodsUnchainedValidatorContract extends BaseContract { @@ -75,6 +77,48 @@ export class GodsUnchainedValidatorContract extends BaseContract { _godsUnchained, ); } + + public static async deployWithLibrariesFrom0xArtifactAsync( + artifact: ContractArtifact, + libraryArtifacts: { [libraryName: string]: ContractArtifact }, + supportedProvider: SupportedProvider, + txDefaults: Partial, + logDecodeDependencies: { [contractName: string]: ContractArtifact | SimpleContractArtifact }, + _godsUnchained: string, + ): Promise { + assert.doesConformToSchema('txDefaults', txDefaults, schemas.txDataSchema, [ + schemas.addressSchema, + schemas.numberSchema, + schemas.jsNumber, + ]); + if (artifact.compilerOutput === undefined) { + throw new Error('Compiler output not found in the artifact file'); + } + const provider = providerUtils.standardizeOrThrow(supportedProvider); + const abi = artifact.compilerOutput.abi; + const logDecodeDependenciesAbiOnly: { [contractName: string]: ContractAbi } = {}; + if (Object.keys(logDecodeDependencies) !== undefined) { + for (const key of Object.keys(logDecodeDependencies)) { + logDecodeDependenciesAbiOnly[key] = logDecodeDependencies[key].compilerOutput.abi; + } + } + const libraryAddresses = await GodsUnchainedValidatorContract._deployLibrariesAsync( + artifact, + libraryArtifacts, + new Web3Wrapper(provider), + txDefaults, + ); + const bytecode = linkLibrariesInBytecode(artifact, libraryAddresses); + return GodsUnchainedValidatorContract.deployAsync( + bytecode, + abi, + provider, + txDefaults, + logDecodeDependenciesAbiOnly, + _godsUnchained, + ); + } + public static async deployAsync( bytecode: string, abi: ContractAbi, @@ -160,12 +204,58 @@ export class GodsUnchainedValidatorContract extends BaseContract { return abi; } + protected static async _deployLibrariesAsync( + artifact: ContractArtifact, + libraryArtifacts: { [libraryName: string]: ContractArtifact }, + web3Wrapper: Web3Wrapper, + txDefaults: Partial, + libraryAddresses: { [libraryName: string]: string } = {}, + ): Promise<{ [libraryName: string]: string }> { + const links = artifact.compilerOutput.evm.bytecode.linkReferences; + // Go through all linked libraries, recursively deploying them if necessary. + for (const link of Object.values(links)) { + for (const libraryName of Object.keys(link)) { + if (!libraryAddresses[libraryName]) { + // Library not yet deployed. + const libraryArtifact = libraryArtifacts[libraryName]; + if (!libraryArtifact) { + throw new Error(`Missing artifact for linked library "${libraryName}"`); + } + // Deploy any dependent libraries used by this library. + await GodsUnchainedValidatorContract._deployLibrariesAsync( + libraryArtifact, + libraryArtifacts, + web3Wrapper, + txDefaults, + libraryAddresses, + ); + // Deploy this library. + const linkedLibraryBytecode = linkLibrariesInBytecode(libraryArtifact, libraryAddresses); + const txDataWithDefaults = await BaseContract._applyDefaultsToContractTxDataAsync( + { + data: linkedLibraryBytecode, + ...txDefaults, + }, + web3Wrapper.estimateGasAsync.bind(web3Wrapper), + ); + const txHash = await web3Wrapper.sendTransactionAsync(txDataWithDefaults); + logUtils.log(`transactionHash: ${txHash}`); + const { contractAddress } = await web3Wrapper.awaitTransactionSuccessAsync(txHash); + logUtils.log(`${libraryArtifact.contractName} successfully deployed at ${contractAddress}`); + libraryAddresses[libraryArtifact.contractName] = contractAddress as string; + } + } + } + return libraryAddresses; + } + public getFunctionSignature(methodName: string): string { const index = this._methodABIIndex[methodName]; const methodAbi = GodsUnchainedValidatorContract.ABI()[index] as MethodAbi; // tslint:disable-line:no-unnecessary-type-assertion const functionSignature = methodAbiToFunctionSignature(methodAbi); return functionSignature; } + public getABIDecodedTransactionData(methodName: string, callData: string): T { const functionSignature = this.getFunctionSignature(methodName); const self = (this as any) as GodsUnchainedValidatorContract; @@ -173,6 +263,7 @@ export class GodsUnchainedValidatorContract extends BaseContract { const abiDecodedCallData = abiEncoder.strictDecode(callData); return abiDecodedCallData; } + public getABIDecodedReturnData(methodName: string, callData: string): T { const functionSignature = this.getFunctionSignature(methodName); const self = (this as any) as GodsUnchainedValidatorContract; @@ -180,6 +271,7 @@ export class GodsUnchainedValidatorContract extends BaseContract { const abiDecodedCallData = abiEncoder.strictDecodeReturnValue(callData); return abiDecodedCallData; } + public getSelector(methodName: string): string { const functionSignature = this.getFunctionSignature(methodName); const self = (this as any) as GodsUnchainedValidatorContract; diff --git a/packages/contract-wrappers/src/generated-wrappers/maximum_gas_price.ts b/packages/contract-wrappers/src/generated-wrappers/maximum_gas_price.ts new file mode 100644 index 0000000000..0a8b09e012 --- /dev/null +++ b/packages/contract-wrappers/src/generated-wrappers/maximum_gas_price.ts @@ -0,0 +1,334 @@ +// tslint:disable:no-consecutive-blank-lines ordered-imports align trailing-comma enum-naming +// tslint:disable:whitespace no-unbound-method no-trailing-whitespace +// tslint:disable:no-unused-variable +import { + AwaitTransactionSuccessOpts, + ContractFunctionObj, + ContractTxFunctionObj, + SendTransactionOpts, + BaseContract, + PromiseWithTransactionHash, + methodAbiToFunctionSignature, + linkLibrariesInBytecode, +} from '@0x/base-contract'; +import { schemas } from '@0x/json-schemas'; +import { + BlockParam, + BlockParamLiteral, + BlockRange, + CallData, + ContractAbi, + ContractArtifact, + DecodedLogArgs, + MethodAbi, + TransactionReceiptWithDecodedLogs, + TxData, + TxDataPayable, + SupportedProvider, +} from 'ethereum-types'; +import { BigNumber, classUtils, hexUtils, logUtils, providerUtils } from '@0x/utils'; +import { EventCallback, IndexedFilterValues, SimpleContractArtifact } from '@0x/types'; +import { Web3Wrapper } from '@0x/web3-wrapper'; +import { assert } from '@0x/assert'; +import * as ethers from 'ethers'; +// tslint:enable:no-unused-variable + +/* istanbul ignore next */ +// tslint:disable:array-type +// tslint:disable:no-parameter-reassignment +// tslint:disable-next-line:class-name +export class MaximumGasPriceContract extends BaseContract { + /** + * @ignore + */ + public static deployedBytecode: string | undefined; + public static contractName = 'MaximumGasPrice'; + private readonly _methodABIIndex: { [name: string]: number } = {}; + public static async deployFrom0xArtifactAsync( + artifact: ContractArtifact | SimpleContractArtifact, + supportedProvider: SupportedProvider, + txDefaults: Partial, + logDecodeDependencies: { [contractName: string]: ContractArtifact | SimpleContractArtifact }, + ): Promise { + assert.doesConformToSchema('txDefaults', txDefaults, schemas.txDataSchema, [ + schemas.addressSchema, + schemas.numberSchema, + schemas.jsNumber, + ]); + if (artifact.compilerOutput === undefined) { + throw new Error('Compiler output not found in the artifact file'); + } + const provider = providerUtils.standardizeOrThrow(supportedProvider); + const bytecode = artifact.compilerOutput.evm.bytecode.object; + const abi = artifact.compilerOutput.abi; + const logDecodeDependenciesAbiOnly: { [contractName: string]: ContractAbi } = {}; + if (Object.keys(logDecodeDependencies) !== undefined) { + for (const key of Object.keys(logDecodeDependencies)) { + logDecodeDependenciesAbiOnly[key] = logDecodeDependencies[key].compilerOutput.abi; + } + } + return MaximumGasPriceContract.deployAsync(bytecode, abi, provider, txDefaults, logDecodeDependenciesAbiOnly); + } + + public static async deployWithLibrariesFrom0xArtifactAsync( + artifact: ContractArtifact, + libraryArtifacts: { [libraryName: string]: ContractArtifact }, + supportedProvider: SupportedProvider, + txDefaults: Partial, + logDecodeDependencies: { [contractName: string]: ContractArtifact | SimpleContractArtifact }, + ): Promise { + assert.doesConformToSchema('txDefaults', txDefaults, schemas.txDataSchema, [ + schemas.addressSchema, + schemas.numberSchema, + schemas.jsNumber, + ]); + if (artifact.compilerOutput === undefined) { + throw new Error('Compiler output not found in the artifact file'); + } + const provider = providerUtils.standardizeOrThrow(supportedProvider); + const abi = artifact.compilerOutput.abi; + const logDecodeDependenciesAbiOnly: { [contractName: string]: ContractAbi } = {}; + if (Object.keys(logDecodeDependencies) !== undefined) { + for (const key of Object.keys(logDecodeDependencies)) { + logDecodeDependenciesAbiOnly[key] = logDecodeDependencies[key].compilerOutput.abi; + } + } + const libraryAddresses = await MaximumGasPriceContract._deployLibrariesAsync( + artifact, + libraryArtifacts, + new Web3Wrapper(provider), + txDefaults, + ); + const bytecode = linkLibrariesInBytecode(artifact, libraryAddresses); + return MaximumGasPriceContract.deployAsync(bytecode, abi, provider, txDefaults, logDecodeDependenciesAbiOnly); + } + + public static async deployAsync( + bytecode: string, + abi: ContractAbi, + supportedProvider: SupportedProvider, + txDefaults: Partial, + logDecodeDependencies: { [contractName: string]: ContractAbi }, + ): Promise { + assert.isHexString('bytecode', bytecode); + assert.doesConformToSchema('txDefaults', txDefaults, schemas.txDataSchema, [ + schemas.addressSchema, + schemas.numberSchema, + schemas.jsNumber, + ]); + const provider = providerUtils.standardizeOrThrow(supportedProvider); + const constructorAbi = BaseContract._lookupConstructorAbi(abi); + [] = BaseContract._formatABIDataItemList(constructorAbi.inputs, [], BaseContract._bigNumberToString); + const iface = new ethers.utils.Interface(abi); + const deployInfo = iface.deployFunction; + const txData = deployInfo.encode(bytecode, []); + const web3Wrapper = new Web3Wrapper(provider); + const txDataWithDefaults = await BaseContract._applyDefaultsToContractTxDataAsync( + { + data: txData, + ...txDefaults, + }, + web3Wrapper.estimateGasAsync.bind(web3Wrapper), + ); + const txHash = await web3Wrapper.sendTransactionAsync(txDataWithDefaults); + logUtils.log(`transactionHash: ${txHash}`); + const txReceipt = await web3Wrapper.awaitTransactionSuccessAsync(txHash); + logUtils.log(`MaximumGasPrice successfully deployed at ${txReceipt.contractAddress}`); + const contractInstance = new MaximumGasPriceContract( + txReceipt.contractAddress as string, + provider, + txDefaults, + logDecodeDependencies, + ); + contractInstance.constructorArgs = []; + return contractInstance; + } + + /** + * @returns The contract ABI + */ + public static ABI(): ContractAbi { + const abi = [ + { + constant: true, + inputs: [], + name: 'checkGasPrice', + outputs: [], + payable: false, + stateMutability: 'view', + type: 'function', + }, + { + constant: true, + inputs: [ + { + name: 'maxGasPrice', + type: 'uint256', + }, + ], + name: 'checkGasPrice', + outputs: [], + payable: false, + stateMutability: 'view', + type: 'function', + }, + ] as ContractAbi; + return abi; + } + + protected static async _deployLibrariesAsync( + artifact: ContractArtifact, + libraryArtifacts: { [libraryName: string]: ContractArtifact }, + web3Wrapper: Web3Wrapper, + txDefaults: Partial, + libraryAddresses: { [libraryName: string]: string } = {}, + ): Promise<{ [libraryName: string]: string }> { + const links = artifact.compilerOutput.evm.bytecode.linkReferences; + // Go through all linked libraries, recursively deploying them if necessary. + for (const link of Object.values(links)) { + for (const libraryName of Object.keys(link)) { + if (!libraryAddresses[libraryName]) { + // Library not yet deployed. + const libraryArtifact = libraryArtifacts[libraryName]; + if (!libraryArtifact) { + throw new Error(`Missing artifact for linked library "${libraryName}"`); + } + // Deploy any dependent libraries used by this library. + await MaximumGasPriceContract._deployLibrariesAsync( + libraryArtifact, + libraryArtifacts, + web3Wrapper, + txDefaults, + libraryAddresses, + ); + // Deploy this library. + const linkedLibraryBytecode = linkLibrariesInBytecode(libraryArtifact, libraryAddresses); + const txDataWithDefaults = await BaseContract._applyDefaultsToContractTxDataAsync( + { + data: linkedLibraryBytecode, + ...txDefaults, + }, + web3Wrapper.estimateGasAsync.bind(web3Wrapper), + ); + const txHash = await web3Wrapper.sendTransactionAsync(txDataWithDefaults); + logUtils.log(`transactionHash: ${txHash}`); + const { contractAddress } = await web3Wrapper.awaitTransactionSuccessAsync(txHash); + logUtils.log(`${libraryArtifact.contractName} successfully deployed at ${contractAddress}`); + libraryAddresses[libraryArtifact.contractName] = contractAddress as string; + } + } + } + return libraryAddresses; + } + + public getFunctionSignature(methodName: string): string { + const index = this._methodABIIndex[methodName]; + const methodAbi = MaximumGasPriceContract.ABI()[index] as MethodAbi; // tslint:disable-line:no-unnecessary-type-assertion + const functionSignature = methodAbiToFunctionSignature(methodAbi); + return functionSignature; + } + + public getABIDecodedTransactionData(methodName: string, callData: string): T { + const functionSignature = this.getFunctionSignature(methodName); + const self = (this as any) as MaximumGasPriceContract; + const abiEncoder = self._lookupAbiEncoder(functionSignature); + const abiDecodedCallData = abiEncoder.strictDecode(callData); + return abiDecodedCallData; + } + + public getABIDecodedReturnData(methodName: string, callData: string): T { + const functionSignature = this.getFunctionSignature(methodName); + const self = (this as any) as MaximumGasPriceContract; + const abiEncoder = self._lookupAbiEncoder(functionSignature); + const abiDecodedCallData = abiEncoder.strictDecodeReturnValue(callData); + return abiDecodedCallData; + } + + public getSelector(methodName: string): string { + const functionSignature = this.getFunctionSignature(methodName); + const self = (this as any) as MaximumGasPriceContract; + const abiEncoder = self._lookupAbiEncoder(functionSignature); + return abiEncoder.getSelector(); + } + + /** + * Checks that the current transaction's gas price is less than + * the default maximum value of 20 Gwei. + */ + public checkGasPrice1(): ContractFunctionObj { + const self = (this as any) as MaximumGasPriceContract; + const functionSignature = 'checkGasPrice()'; + + return { + async callAsync(callData: Partial = {}, defaultBlock?: BlockParam): Promise { + BaseContract._assertCallParams(callData, defaultBlock); + const rawCallResult = await self._performCallAsync( + { ...callData, data: this.getABIEncodedTransactionData() }, + defaultBlock, + ); + const abiEncoder = self._lookupAbiEncoder(functionSignature); + BaseContract._throwIfUnexpectedEmptyCallResult(rawCallResult, abiEncoder); + return abiEncoder.strictDecodeReturnValue(rawCallResult); + }, + getABIEncodedTransactionData(): string { + return self._strictEncodeArguments(functionSignature, []); + }, + }; + } + /** + * Checks that the current transaction's gas price is less than + * the specified maximum value. + * @param maxGasPrice The maximum gas price allowed for the current + * transaction. + */ + public checkGasPrice2(maxGasPrice: BigNumber): ContractFunctionObj { + const self = (this as any) as MaximumGasPriceContract; + assert.isBigNumber('maxGasPrice', maxGasPrice); + const functionSignature = 'checkGasPrice(uint256)'; + + return { + async callAsync(callData: Partial = {}, defaultBlock?: BlockParam): Promise { + BaseContract._assertCallParams(callData, defaultBlock); + const rawCallResult = await self._performCallAsync( + { ...callData, data: this.getABIEncodedTransactionData() }, + defaultBlock, + ); + const abiEncoder = self._lookupAbiEncoder(functionSignature); + BaseContract._throwIfUnexpectedEmptyCallResult(rawCallResult, abiEncoder); + return abiEncoder.strictDecodeReturnValue(rawCallResult); + }, + getABIEncodedTransactionData(): string { + return self._strictEncodeArguments(functionSignature, [maxGasPrice]); + }, + }; + } + + constructor( + address: string, + supportedProvider: SupportedProvider, + txDefaults?: Partial, + logDecodeDependencies?: { [contractName: string]: ContractAbi }, + deployedBytecode: string | undefined = MaximumGasPriceContract.deployedBytecode, + ) { + super( + 'MaximumGasPrice', + MaximumGasPriceContract.ABI(), + address, + supportedProvider, + txDefaults, + logDecodeDependencies, + deployedBytecode, + ); + classUtils.bindAll(this, ['_abiEncoderByFunctionSignature', 'address', '_web3Wrapper']); + MaximumGasPriceContract.ABI().forEach((item, index) => { + if (item.type === 'function') { + const methodAbi = item as MethodAbi; + this._methodABIIndex[methodAbi.name] = index; + } + }); + } +} + +// tslint:disable:max-file-line-count +// tslint:enable:no-unbound-method no-parameter-reassignment no-consecutive-blank-lines ordered-imports align +// tslint:enable:trailing-comma whitespace no-trailing-whitespace diff --git a/packages/migrations/src/migration.ts b/packages/migrations/src/migration.ts index 5e5e8bd5db..42d4f4a6e7 100644 --- a/packages/migrations/src/migration.ts +++ b/packages/migrations/src/migration.ts @@ -307,6 +307,7 @@ export async function runMigrationsAsync( godsUnchainedValidator: constants.NULL_ADDRESS, broker: constants.NULL_ADDRESS, chainlinkStopLimit: constants.NULL_ADDRESS, + maximumGasPrice: constants.NULL_ADDRESS, }; return contractAddresses; }