diff --git a/contracts/asset-proxy/CHANGELOG.json b/contracts/asset-proxy/CHANGELOG.json index d9d6329dc4..e51f943a2b 100644 --- a/contracts/asset-proxy/CHANGELOG.json +++ b/contracts/asset-proxy/CHANGELOG.json @@ -25,6 +25,10 @@ { "note": "Add `Eth2DaiBridge`", "pr": 2221 + }, + { + "note": "Add `UniswapBridge`", + "pr": 2233 } ], "timestamp": 1570135330 diff --git a/contracts/asset-proxy/contracts/src/bridges/Eth2DaiBridge.sol b/contracts/asset-proxy/contracts/src/bridges/Eth2DaiBridge.sol index d724dc5c51..8cba2ca305 100644 --- a/contracts/asset-proxy/contracts/src/bridges/Eth2DaiBridge.sol +++ b/contracts/asset-proxy/contracts/src/bridges/Eth2DaiBridge.sol @@ -20,9 +20,9 @@ pragma solidity ^0.5.9; pragma experimental ABIEncoderV2; import "@0x/contracts-erc20/contracts/src/interfaces/IERC20Token.sol"; +import "@0x/contracts-exchange-libs/contracts/src/IWallet.sol"; import "../interfaces/IERC20Bridge.sol"; import "../interfaces/IEth2Dai.sol"; -import "../interfaces/IWallet.sol"; // solhint-disable space-after-comma diff --git a/contracts/asset-proxy/contracts/src/bridges/UniswapBridge.sol b/contracts/asset-proxy/contracts/src/bridges/UniswapBridge.sol new file mode 100644 index 0000000000..c5210d7e96 --- /dev/null +++ b/contracts/asset-proxy/contracts/src/bridges/UniswapBridge.sol @@ -0,0 +1,218 @@ +/* + + 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.9; +pragma experimental ABIEncoderV2; + +import "@0x/contracts-erc20/contracts/src/interfaces/IERC20Token.sol"; +import "@0x/contracts-erc20/contracts/src/interfaces/IEtherToken.sol"; +import "@0x/contracts-exchange-libs/contracts/src/IWallet.sol"; +import "../interfaces/IUniswapExchangeFactory.sol"; +import "../interfaces/IUniswapExchange.sol"; +import "../interfaces/IERC20Bridge.sol"; + + +// solhint-disable space-after-comma +// solhint-disable not-rely-on-time +contract UniswapBridge is + IERC20Bridge, + IWallet +{ + /* Mainnet addresses */ + address constant private UNISWAP_EXCHANGE_FACTORY_ADDRESS = 0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95; + address constant private WETH_ADDRESS = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + + // Struct to hold `withdrawTo()` local variables in memory and to avoid + // stack overflows. + struct WithdrawToState { + IUniswapExchange exchange; + uint256 fromTokenBalance; + IEtherToken weth; + } + + // solhint-disable no-empty-blocks + /// @dev Payable fallback to receive ETH from uniswap. + function () + external + payable + {} + + /// @dev Callback for `IERC20Bridge`. Tries to buy `amount` of + /// `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 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 withdrawTo( + address toTokenAddress, + address /* from */, + address to, + uint256 amount, + bytes calldata bridgeData + ) + external + returns (bytes4 success) + { + // State memory object to avoid stack overflows. + WithdrawToState memory state; + // Decode the bridge data to get the `fromTokenAddress`. + (address fromTokenAddress) = abi.decode(bridgeData, (address)); + + // Just transfer the tokens if they're the same. + if (fromTokenAddress == toTokenAddress) { + IERC20Token(fromTokenAddress).transfer(to, amount); + return BRIDGE_SUCCESS; + } + + // Get the exchange for the token pair. + state.exchange = _getUniswapExchangeForTokenPair( + fromTokenAddress, + toTokenAddress + ); + // Get our balance of `fromTokenAddress` token. + state.fromTokenBalance = IERC20Token(fromTokenAddress).balanceOf(address(this)); + // Get the weth contract. + state.weth = getWethContract(); + + // Convert from WETH to a token. + if (fromTokenAddress == address(state.weth)) { + // Unwrap the WETH. + 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)( + // Minimum buy amount. + amount, + // Expires after this block. + block.timestamp, + // Recipient is `to`. + to + ); + + // Convert from a token to WETH. + } else if (toTokenAddress == address(state.weth)) { + // Grant the exchange an allowance. + _grantExchangeAllowance(state.exchange, fromTokenAddress); + // Buy as much ETH with `fromTokenAddress` token as possible. + uint256 ethBought = state.exchange.tokenToEthSwapInput( + // Sell all tokens we hold. + state.fromTokenBalance, + // Minimum buy amount. + amount, + // Expires after this block. + block.timestamp + ); + // Wrap the ETH. + state.weth.deposit.value(ethBought)(); + // Transfer the WETH to `to`. + IEtherToken(toTokenAddress).transfer(to, ethBought); + + // Convert from one token to another. + } else { + // Grant the exchange an allowance. + _grantExchangeAllowance(state.exchange, fromTokenAddress); + // Buy as much `toTokenAddress` token with `fromTokenAddress` token + // and transfer it to `to`. + state.exchange.tokenToTokenTransferInput( + // Sell all tokens we hold. + state.fromTokenBalance, + // Minimum buy amount. + amount, + // No minimum intermediate ETH buy amount. + 0, + // Expires after this block. + block.timestamp, + // Recipient is `to`. + to, + // Convert to `toTokenAddress`. + toTokenAddress + ); + } + return BRIDGE_SUCCESS; + } + + /// @dev `SignatureType.Wallet` callback, so that this bridge can be the maker + /// and sign for itself in orders. Always succeeds. + /// @return magicValue Success bytes, always. + function isValidSignature( + bytes32, + bytes calldata + ) + external + view + returns (bytes4 magicValue) + { + return LEGACY_WALLET_MAGIC_VALUE; + } + + /// @dev Overridable way to get the weth contract. + /// @return token The WETH contract. + function getWethContract() + public + view + returns (IEtherToken token) + { + return IEtherToken(WETH_ADDRESS); + } + + /// @dev Overridable way to get the uniswap exchange factory contract. + /// @return factory The exchange factory contract. + function getUniswapExchangeFactoryContract() + public + view + returns (IUniswapExchangeFactory factory) + { + return IUniswapExchangeFactory(UNISWAP_EXCHANGE_FACTORY_ADDRESS); + } + + /// @dev Grants an unlimited allowance to the exchange for its token + /// 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) + private + { + IERC20Token(tokenAddress).approve(address(exchange), uint256(-1)); + } + + /// @dev Retrieves the uniswap exchange for a given token pair. + /// In the case of a WETH-token exchange, this will be the non-WETH token. + /// In th ecase of a token-token exchange, this will be the first token. + /// @param fromTokenAddress The address of the token we are converting from. + /// @param toTokenAddress The address of the token we are converting to. + /// @return exchange The uniswap exchange. + function _getUniswapExchangeForTokenPair( + address fromTokenAddress, + address toTokenAddress + ) + private + view + returns (IUniswapExchange exchange) + { + address exchangeTokenAddress = fromTokenAddress; + // Whichever isn't WETH is the exchange token. + if (fromTokenAddress == address(getWethContract())) { + exchangeTokenAddress = toTokenAddress; + } + exchange = getUniswapExchangeFactoryContract().getExchange(exchangeTokenAddress); + require(address(exchange) != address(0), "NO_UNISWAP_EXCHANGE_FOR_TOKEN"); + return exchange; + } +} diff --git a/contracts/asset-proxy/contracts/src/interfaces/IUniswapExchange.sol b/contracts/asset-proxy/contracts/src/interfaces/IUniswapExchange.sol new file mode 100644 index 0000000000..20fa61ab58 --- /dev/null +++ b/contracts/asset-proxy/contracts/src/interfaces/IUniswapExchange.sol @@ -0,0 +1,77 @@ +/* + + 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.9; + + +interface IUniswapExchange { + + /// @dev Buys at least `minTokensBought` tokens with ETH and transfer them + /// to `recipient`. + /// @param minTokensBought The minimum number of tokens to buy. + /// @param deadline Time when this order expires. + /// @param recipient Who to transfer the tokens to. + /// @return tokensBought Amount of tokens bought. + function ethToTokenTransferInput( + uint256 minTokensBought, + uint256 deadline, + address recipient + ) + external + payable + returns (uint256 tokensBought); + + /// @dev Buys at least `minEthBought` ETH with tokens. + /// @param tokensSold Amount of tokens to sell. + /// @param minEthBought The minimum amount of ETH to buy. + /// @param deadline Time when this order expires. + /// @return ethBought Amount of tokens bought. + function tokenToEthSwapInput( + uint256 tokensSold, + uint256 minEthBought, + uint256 deadline + ) + external + returns (uint256 ethBought); + + /// @dev Buys at least `minTokensBought` tokens with the exchange token + /// and transfer them to `recipient`. + /// @param minTokensBought The minimum number of tokens to buy. + /// @param minEthBought The minimum amount of intermediate ETH to buy. + /// @param deadline Time when this order expires. + /// @param recipient Who to transfer the tokens to. + /// @param toTokenAddress The token being bought. + /// @return tokensBought Amount of tokens bought. + function tokenToTokenTransferInput( + uint256 tokensSold, + uint256 minTokensBought, + uint256 minEthBought, + uint256 deadline, + address recipient, + address toTokenAddress + ) + external + returns (uint256 tokensBought); + + /// @dev Retrieves the token that is associated with this exchange. + /// @return tokenAddress The token address. + function toTokenAddress() + external + view + returns (address tokenAddress); +} diff --git a/contracts/asset-proxy/contracts/src/interfaces/IUniswapExchangeFactory.sol b/contracts/asset-proxy/contracts/src/interfaces/IUniswapExchangeFactory.sol new file mode 100644 index 0000000000..c175f55326 --- /dev/null +++ b/contracts/asset-proxy/contracts/src/interfaces/IUniswapExchangeFactory.sol @@ -0,0 +1,32 @@ +/* + + 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.9; + +import "./IUniswapExchange.sol"; + + +interface IUniswapExchangeFactory { + + /// @dev Get the exchange for a token. + /// @param tokenAddress The address of the token contract. + function getExchange(address tokenAddress) + external + view + returns (IUniswapExchange); +} diff --git a/contracts/asset-proxy/contracts/test/TestUniswapBridge.sol b/contracts/asset-proxy/contracts/test/TestUniswapBridge.sol new file mode 100644 index 0000000000..9f67714a1b --- /dev/null +++ b/contracts/asset-proxy/contracts/test/TestUniswapBridge.sol @@ -0,0 +1,432 @@ +/* + + 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.9; +pragma experimental ABIEncoderV2; + +import "@0x/contracts-erc20/contracts/src/interfaces/IERC20Token.sol"; +import "@0x/contracts-utils/contracts/src/LibSafeMath.sol"; +import "../src/bridges/UniswapBridge.sol"; +import "../src/interfaces/IUniswapExchangeFactory.sol"; +import "../src/interfaces/IUniswapExchange.sol"; + + +// solhint-disable no-simple-event-func-name +contract TestEventsRaiser { + + event TokenTransfer( + address token, + address from, + address to, + uint256 amount + ); + + event TokenApprove( + address spender, + uint256 allowance + ); + + event WethDeposit( + uint256 amount + ); + + event WethWithdraw( + uint256 amount + ); + + event EthToTokenTransferInput( + address exchange, + uint256 minTokensBought, + uint256 deadline, + address recipient + ); + + event TokenToEthSwapInput( + address exchange, + uint256 tokensSold, + uint256 minEthBought, + uint256 deadline + ); + + event TokenToTokenTransferInput( + address exchange, + uint256 tokensSold, + uint256 minTokensBought, + uint256 minEthBought, + uint256 deadline, + address recipient, + address toTokenAddress + ); + + function raiseEthToTokenTransferInput( + uint256 minTokensBought, + uint256 deadline, + address recipient + ) + external + { + emit EthToTokenTransferInput( + msg.sender, + minTokensBought, + deadline, + recipient + ); + } + + function raiseTokenToEthSwapInput( + uint256 tokensSold, + uint256 minEthBought, + uint256 deadline + ) + external + { + emit TokenToEthSwapInput( + msg.sender, + tokensSold, + minEthBought, + deadline + ); + } + + function raiseTokenToTokenTransferInput( + uint256 tokensSold, + uint256 minTokensBought, + uint256 minEthBought, + uint256 deadline, + address recipient, + address toTokenAddress + ) + external + { + emit TokenToTokenTransferInput( + msg.sender, + tokensSold, + minTokensBought, + minEthBought, + deadline, + recipient, + toTokenAddress + ); + } + + function raiseTokenTransfer( + address from, + address to, + uint256 amount + ) + external + { + emit TokenTransfer( + msg.sender, + from, + to, + amount + ); + } + + function raiseTokenApprove(address spender, uint256 allowance) + external + { + emit TokenApprove(spender, allowance); + } + + function raiseWethDeposit(uint256 amount) + external + { + emit WethDeposit(amount); + } + + function raiseWethWithdraw(uint256 amount) + external + { + emit WethWithdraw(amount); + } +} + + +/// @dev A minimalist ERC20/WETH token. +contract TestToken { + + using LibSafeMath for uint256; + + mapping (address => uint256) public balances; + string private _nextRevertReason; + + /// @dev Set the balance for `owner`. + function setBalance(address owner) + external + payable + { + balances[owner] = msg.value; + } + + /// @dev Set the revert reason for `transfer()`, + /// `deposit()`, and `withdraw()`. + function setRevertReason(string calldata reason) + external + { + _nextRevertReason = reason; + } + + /// @dev Just calls `raiseTokenTransfer()` on the caller. + function transfer(address to, uint256 amount) + external + returns (bool) + { + _revertIfReasonExists(); + TestEventsRaiser(msg.sender).raiseTokenTransfer(msg.sender, to, amount); + return true; + } + + /// @dev Just calls `raiseTokenApprove()` on the caller. + function approve(address spender, uint256 allowance) + external + returns (bool) + { + TestEventsRaiser(msg.sender).raiseTokenApprove(spender, allowance); + return true; + } + + /// @dev `IWETH.deposit()` that increases balances and calls + /// `raiseWethDeposit()` on the caller. + function deposit() + external + payable + { + _revertIfReasonExists(); + balances[msg.sender] += balances[msg.sender].safeAdd(msg.value); + TestEventsRaiser(msg.sender).raiseWethDeposit(msg.value); + } + + /// @dev `IWETH.withdraw()` that just reduces balances and calls + /// `raiseWethWithdraw()` on the caller. + function withdraw(uint256 amount) + external + { + _revertIfReasonExists(); + balances[msg.sender] = balances[msg.sender].safeSub(amount); + msg.sender.transfer(amount); + TestEventsRaiser(msg.sender).raiseWethWithdraw(amount); + } + + /// @dev Retrieve the balance for `owner`. + function balanceOf(address owner) + external + view + returns (uint256) + { + return balances[owner]; + } + + function _revertIfReasonExists() + private + view + { + if (bytes(_nextRevertReason).length != 0) { + revert(_nextRevertReason); + } + } +} + + +contract TestExchange is + IUniswapExchange +{ + address public tokenAddress; + string private _nextRevertReason; + + constructor(address _tokenAddress) public { + tokenAddress = _tokenAddress; + } + + function setFillBehavior( + string calldata revertReason + ) + external + payable + { + _nextRevertReason = revertReason; + } + + function ethToTokenTransferInput( + uint256 minTokensBought, + uint256 deadline, + address recipient + ) + external + payable + returns (uint256 tokensBought) + { + TestEventsRaiser(msg.sender).raiseEthToTokenTransferInput( + minTokensBought, + deadline, + recipient + ); + _revertIfReasonExists(); + return address(this).balance; + } + + function tokenToEthSwapInput( + uint256 tokensSold, + uint256 minEthBought, + uint256 deadline + ) + external + returns (uint256 ethBought) + { + TestEventsRaiser(msg.sender).raiseTokenToEthSwapInput( + tokensSold, + minEthBought, + deadline + ); + _revertIfReasonExists(); + uint256 fillAmount = address(this).balance; + msg.sender.transfer(fillAmount); + return fillAmount; + } + + function tokenToTokenTransferInput( + uint256 tokensSold, + uint256 minTokensBought, + uint256 minEthBought, + uint256 deadline, + address recipient, + address toTokenAddress + ) + external + returns (uint256 tokensBought) + { + TestEventsRaiser(msg.sender).raiseTokenToTokenTransferInput( + tokensSold, + minTokensBought, + minEthBought, + deadline, + recipient, + toTokenAddress + ); + _revertIfReasonExists(); + return address(this).balance; + } + + function toTokenAddress() + external + view + returns (address _tokenAddress) + { + return tokenAddress; + } + + function _revertIfReasonExists() + private + view + { + if (bytes(_nextRevertReason).length != 0) { + revert(_nextRevertReason); + } + } +} + + +/// @dev UniswapBridge overridden to mock tokens and implement IUniswapExchangeFactory. +contract TestUniswapBridge is + IUniswapExchangeFactory, + TestEventsRaiser, + UniswapBridge +{ + TestToken public wethToken; + // Token address to TestToken instance. + mapping (address => TestToken) private _testTokens; + // Token address to TestExchange instance. + mapping (address => TestExchange) private _testExchanges; + + constructor() public { + wethToken = new TestToken(); + _testTokens[address(wethToken)] = wethToken; + } + + /// @dev Sets the balance of this contract for an existing token. + /// The wei attached will be the balance. + function setTokenBalance(address tokenAddress) + external + payable + { + TestToken token = _testTokens[tokenAddress]; + token.deposit.value(msg.value)(); + } + + /// @dev Sets the revert reason for an existing token. + function setTokenRevertReason(address tokenAddress, string calldata revertReason) + external + { + TestToken token = _testTokens[tokenAddress]; + token.setRevertReason(revertReason); + } + + /// @dev Create a token and exchange (if they don't exist) for a new token + /// and sets the exchange revert and fill behavior. The wei attached + /// will be the fill amount for the exchange. + /// @param tokenAddress The token address. If zero, one will be created. + /// @param revertReason The revert reason for exchange operations. + function createTokenAndExchange( + address tokenAddress, + string calldata revertReason + ) + external + payable + returns (TestToken token, TestExchange exchange) + { + token = TestToken(tokenAddress); + if (tokenAddress == address(0)) { + token = new TestToken(); + } + _testTokens[address(token)] = token; + exchange = _testExchanges[address(token)]; + if (address(exchange) == address(0)) { + _testExchanges[address(token)] = exchange = new TestExchange(address(token)); + } + exchange.setFillBehavior.value(msg.value)(revertReason); + return (token, exchange); + } + + /// @dev `IUniswapExchangeFactory.getExchange` + function getExchange(address tokenAddress) + external + view + returns (IUniswapExchange) + { + return IUniswapExchange(_testExchanges[tokenAddress]); + } + + // @dev Use `wethToken`. + function getWethContract() + public + view + returns (IEtherToken) + { + return IEtherToken(address(wethToken)); + } + + // @dev This contract will double as the Uniswap contract. + function getUniswapExchangeFactoryContract() + public + view + returns (IUniswapExchangeFactory) + { + return IUniswapExchangeFactory(address(this)); + } +} diff --git a/contracts/asset-proxy/package.json b/contracts/asset-proxy/package.json index bbd5b8a108..a39c3c18b5 100644 --- a/contracts/asset-proxy/package.json +++ b/contracts/asset-proxy/package.json @@ -35,7 +35,7 @@ "compile:truffle": "truffle compile" }, "config": { - "abis": "./generated-artifacts/@(ERC1155Proxy|ERC20BridgeProxy|ERC20Proxy|ERC721Proxy|Eth2DaiBridge|IAssetData|IAssetProxy|IAssetProxyDispatcher|IAuthorizable|IERC20Bridge|IEth2Dai|IWallet|MixinAssetProxyDispatcher|MixinAuthorizable|MultiAssetProxy|Ownable|StaticCallProxy|TestERC20Bridge|TestEth2DaiBridge|TestStaticCallTarget).json", + "abis": "./generated-artifacts/@(ERC1155Proxy|ERC20BridgeProxy|ERC20Proxy|ERC721Proxy|Eth2DaiBridge|IAssetData|IAssetProxy|IAssetProxyDispatcher|IAuthorizable|IERC20Bridge|IEth2Dai|IUniswapExchange|IUniswapExchangeFactory|MixinAssetProxyDispatcher|MixinAuthorizable|MultiAssetProxy|Ownable|StaticCallProxy|TestERC20Bridge|TestEth2DaiBridge|TestStaticCallTarget|TestUniswapBridge|UniswapBridge).json", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually." }, "repository": { diff --git a/contracts/asset-proxy/src/artifacts.ts b/contracts/asset-proxy/src/artifacts.ts index a7145fd6c5..ebe46f9d40 100644 --- a/contracts/asset-proxy/src/artifacts.ts +++ b/contracts/asset-proxy/src/artifacts.ts @@ -16,7 +16,8 @@ import * as IAssetProxyDispatcher from '../generated-artifacts/IAssetProxyDispat import * as IAuthorizable from '../generated-artifacts/IAuthorizable.json'; import * as IERC20Bridge from '../generated-artifacts/IERC20Bridge.json'; import * as IEth2Dai from '../generated-artifacts/IEth2Dai.json'; -import * as IWallet from '../generated-artifacts/IWallet.json'; +import * as IUniswapExchange from '../generated-artifacts/IUniswapExchange.json'; +import * as IUniswapExchangeFactory from '../generated-artifacts/IUniswapExchangeFactory.json'; import * as MixinAssetProxyDispatcher from '../generated-artifacts/MixinAssetProxyDispatcher.json'; import * as MixinAuthorizable from '../generated-artifacts/MixinAuthorizable.json'; import * as MultiAssetProxy from '../generated-artifacts/MultiAssetProxy.json'; @@ -25,6 +26,8 @@ import * as StaticCallProxy from '../generated-artifacts/StaticCallProxy.json'; import * as TestERC20Bridge from '../generated-artifacts/TestERC20Bridge.json'; import * as TestEth2DaiBridge from '../generated-artifacts/TestEth2DaiBridge.json'; import * as TestStaticCallTarget from '../generated-artifacts/TestStaticCallTarget.json'; +import * as TestUniswapBridge from '../generated-artifacts/TestUniswapBridge.json'; +import * as UniswapBridge from '../generated-artifacts/UniswapBridge.json'; export const artifacts = { MixinAssetProxyDispatcher: MixinAssetProxyDispatcher as ContractArtifact, MixinAuthorizable: MixinAuthorizable as ContractArtifact, @@ -36,14 +39,17 @@ export const artifacts = { MultiAssetProxy: MultiAssetProxy as ContractArtifact, StaticCallProxy: StaticCallProxy as ContractArtifact, Eth2DaiBridge: Eth2DaiBridge as ContractArtifact, + UniswapBridge: UniswapBridge as ContractArtifact, IAssetData: IAssetData as ContractArtifact, IAssetProxy: IAssetProxy as ContractArtifact, IAssetProxyDispatcher: IAssetProxyDispatcher as ContractArtifact, IAuthorizable: IAuthorizable as ContractArtifact, IERC20Bridge: IERC20Bridge as ContractArtifact, IEth2Dai: IEth2Dai as ContractArtifact, - IWallet: IWallet as ContractArtifact, + IUniswapExchange: IUniswapExchange as ContractArtifact, + IUniswapExchangeFactory: IUniswapExchangeFactory as ContractArtifact, TestERC20Bridge: TestERC20Bridge as ContractArtifact, TestEth2DaiBridge: TestEth2DaiBridge as ContractArtifact, TestStaticCallTarget: TestStaticCallTarget as ContractArtifact, + TestUniswapBridge: TestUniswapBridge as ContractArtifact, }; diff --git a/contracts/asset-proxy/src/wrappers.ts b/contracts/asset-proxy/src/wrappers.ts index a87eda1f77..a39cbd282b 100644 --- a/contracts/asset-proxy/src/wrappers.ts +++ b/contracts/asset-proxy/src/wrappers.ts @@ -14,7 +14,8 @@ export * from '../generated-wrappers/i_asset_proxy_dispatcher'; export * from '../generated-wrappers/i_authorizable'; export * from '../generated-wrappers/i_erc20_bridge'; export * from '../generated-wrappers/i_eth2_dai'; -export * from '../generated-wrappers/i_wallet'; +export * from '../generated-wrappers/i_uniswap_exchange'; +export * from '../generated-wrappers/i_uniswap_exchange_factory'; export * from '../generated-wrappers/mixin_asset_proxy_dispatcher'; export * from '../generated-wrappers/mixin_authorizable'; export * from '../generated-wrappers/multi_asset_proxy'; @@ -23,3 +24,5 @@ export * from '../generated-wrappers/static_call_proxy'; export * from '../generated-wrappers/test_erc20_bridge'; export * from '../generated-wrappers/test_eth2_dai_bridge'; export * from '../generated-wrappers/test_static_call_target'; +export * from '../generated-wrappers/test_uniswap_bridge'; +export * from '../generated-wrappers/uniswap_bridge'; diff --git a/contracts/asset-proxy/test/uniswap_bridge.ts b/contracts/asset-proxy/test/uniswap_bridge.ts new file mode 100644 index 0000000000..48c6d2883b --- /dev/null +++ b/contracts/asset-proxy/test/uniswap_bridge.ts @@ -0,0 +1,365 @@ +import { + blockchainTests, + constants, + expect, + filterLogs, + filterLogsToArguments, + getRandomInteger, + hexLeftPad, + hexRandom, + Numberish, + randomAddress, + TransactionHelper, +} from '@0x/contracts-test-utils'; +import { AssetProxyId } from '@0x/types'; +import { BigNumber } from '@0x/utils'; +import { DecodedLogs } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { + artifacts, + TestUniswapBridgeContract, + TestUniswapBridgeEthToTokenTransferInputEventArgs as EthToTokenTransferInputArgs, + TestUniswapBridgeEvents as ContractEvents, + TestUniswapBridgeTokenApproveEventArgs as TokenApproveArgs, + TestUniswapBridgeTokenToEthSwapInputEventArgs as TokenToEthSwapInputArgs, + TestUniswapBridgeTokenToTokenTransferInputEventArgs as TokenToTokenTransferInputArgs, + TestUniswapBridgeTokenTransferEventArgs as TokenTransferArgs, + TestUniswapBridgeWethDepositEventArgs as WethDepositArgs, + TestUniswapBridgeWethWithdrawEventArgs as WethWithdrawArgs, +} from '../src'; + +blockchainTests.resets('UniswapBridge unit tests', env => { + const txHelper = new TransactionHelper(env.web3Wrapper, artifacts); + let testContract: TestUniswapBridgeContract; + let wethTokenAddress: string; + + before(async () => { + testContract = await TestUniswapBridgeContract.deployFrom0xArtifactAsync( + artifacts.TestUniswapBridge, + env.provider, + env.txDefaults, + artifacts, + ); + wethTokenAddress = await testContract.wethToken.callAsync(); + }); + + describe('isValidSignature()', () => { + it('returns success bytes', async () => { + const LEGACY_WALLET_MAGIC_VALUE = '0xb0671381'; + const result = await testContract.isValidSignature.callAsync(hexRandom(), hexRandom(_.random(0, 32))); + expect(result).to.eq(LEGACY_WALLET_MAGIC_VALUE); + }); + }); + + describe('withdrawTo()', () => { + interface WithdrawToOpts { + fromTokenAddress: string; + toTokenAddress: string; + fromTokenBalance: Numberish; + toAddress: string; + amount: Numberish; + exchangeRevertReason: string; + exchangeFillAmount: Numberish; + toTokenRevertReason: string; + fromTokenRevertReason: string; + } + + function createWithdrawToOpts(opts?: Partial): WithdrawToOpts { + return { + fromTokenAddress: constants.NULL_ADDRESS, + toTokenAddress: constants.NULL_ADDRESS, + fromTokenBalance: getRandomInteger(1, 1e18), + toAddress: randomAddress(), + amount: getRandomInteger(1, 1e18), + exchangeRevertReason: '', + exchangeFillAmount: getRandomInteger(1, 1e18), + toTokenRevertReason: '', + fromTokenRevertReason: '', + ...opts, + }; + } + + interface WithdrawToResult { + opts: WithdrawToOpts; + result: string; + logs: DecodedLogs; + blockTime: number; + } + + async function withdrawToAsync(opts?: Partial): Promise { + const _opts = createWithdrawToOpts(opts); + // Create the "from" token and exchange. + [[_opts.fromTokenAddress]] = await txHelper.getResultAndReceiptAsync( + testContract.createTokenAndExchange, + _opts.fromTokenAddress, + _opts.exchangeRevertReason, + { value: new BigNumber(_opts.exchangeFillAmount) }, + ); + // Create the "to" token and exchange. + [[_opts.toTokenAddress]] = await txHelper.getResultAndReceiptAsync( + testContract.createTokenAndExchange, + _opts.toTokenAddress, + _opts.exchangeRevertReason, + { value: new BigNumber(_opts.exchangeFillAmount) }, + ); + await testContract.setTokenRevertReason.awaitTransactionSuccessAsync( + _opts.toTokenAddress, + _opts.toTokenRevertReason, + ); + await testContract.setTokenRevertReason.awaitTransactionSuccessAsync( + _opts.fromTokenAddress, + _opts.fromTokenRevertReason, + ); + // Set the token balance for the token we're converting from. + await testContract.setTokenBalance.awaitTransactionSuccessAsync(_opts.fromTokenAddress, { + value: new BigNumber(_opts.fromTokenBalance), + }); + // Call withdrawTo(). + const [result, receipt] = await txHelper.getResultAndReceiptAsync( + testContract.withdrawTo, + // The "to" token address. + _opts.toTokenAddress, + // The "from" address. + randomAddress(), + // The "to" address. + _opts.toAddress, + // The amount to transfer to "to" + new BigNumber(_opts.amount), + // ABI-encoded "from" token address. + hexLeftPad(_opts.fromTokenAddress), + ); + return { + opts: _opts, + result, + logs: (receipt.logs as any) as DecodedLogs, + blockTime: await env.web3Wrapper.getBlockTimestampAsync(receipt.blockNumber), + }; + } + + async function getExchangeForTokenAsync(tokenAddress: string): Promise { + return testContract.getExchange.callAsync(tokenAddress); + } + + it('returns magic bytes on success', async () => { + const { result } = await withdrawToAsync(); + expect(result).to.eq(AssetProxyId.ERC20Bridge); + }); + + it('just transfers tokens to `to` if the same tokens are in play', async () => { + const [[tokenAddress]] = await txHelper.getResultAndReceiptAsync( + testContract.createTokenAndExchange, + constants.NULL_ADDRESS, + '', + ); + const { opts, result, logs } = await withdrawToAsync({ + fromTokenAddress: tokenAddress, + toTokenAddress: tokenAddress, + }); + expect(result).to.eq(AssetProxyId.ERC20Bridge); + const transfers = filterLogsToArguments(logs, ContractEvents.TokenTransfer); + expect(transfers.length).to.eq(1); + expect(transfers[0].token).to.eq(tokenAddress); + expect(transfers[0].from).to.eq(testContract.address); + expect(transfers[0].to).to.eq(opts.toAddress); + expect(transfers[0].amount).to.bignumber.eq(opts.amount); + }); + + describe('token -> token', () => { + it('calls `IUniswapExchange.tokenToTokenTransferInput()', async () => { + const { opts, logs, blockTime } = await withdrawToAsync(); + const exchangeAddress = await getExchangeForTokenAsync(opts.fromTokenAddress); + const calls = filterLogsToArguments( + logs, + ContractEvents.TokenToTokenTransferInput, + ); + expect(calls.length).to.eq(1); + expect(calls[0].exchange).to.eq(exchangeAddress); + expect(calls[0].tokensSold).to.bignumber.eq(opts.fromTokenBalance); + expect(calls[0].minTokensBought).to.bignumber.eq(opts.amount); + expect(calls[0].minEthBought).to.bignumber.eq(0); + expect(calls[0].deadline).to.bignumber.eq(blockTime); + expect(calls[0].recipient).to.eq(opts.toAddress); + expect(calls[0].toTokenAddress).to.eq(opts.toTokenAddress); + }); + + it('sets allowance for "from" token', async () => { + const { opts, logs } = await withdrawToAsync(); + const approvals = filterLogsToArguments(logs, ContractEvents.TokenApprove); + const exchangeAddress = await getExchangeForTokenAsync(opts.fromTokenAddress); + expect(approvals.length).to.eq(1); + expect(approvals[0].spender).to.eq(exchangeAddress); + expect(approvals[0].allowance).to.bignumber.eq(constants.MAX_UINT256); + }); + + it('sets allowance for "from" token on subsequent calls', async () => { + const { opts } = await withdrawToAsync(); + const { logs } = await withdrawToAsync(opts); + const approvals = filterLogsToArguments(logs, ContractEvents.TokenApprove); + const exchangeAddress = await getExchangeForTokenAsync(opts.fromTokenAddress); + expect(approvals.length).to.eq(1); + expect(approvals[0].spender).to.eq(exchangeAddress); + expect(approvals[0].allowance).to.bignumber.eq(constants.MAX_UINT256); + }); + + it('fails if "from" token does not exist', async () => { + const tx = testContract.withdrawTo.awaitTransactionSuccessAsync( + randomAddress(), + randomAddress(), + randomAddress(), + getRandomInteger(1, 1e18), + hexLeftPad(randomAddress()), + ); + return expect(tx).to.revertWith('NO_UNISWAP_EXCHANGE_FOR_TOKEN'); + }); + + it('fails if the exchange fails', async () => { + const revertReason = 'FOOBAR'; + const tx = withdrawToAsync({ + exchangeRevertReason: revertReason, + }); + return expect(tx).to.revertWith(revertReason); + }); + }); + + describe('token -> ETH', () => { + it('calls `IUniswapExchange.tokenToEthSwapInput()`, `WETH.deposit()`, then `transfer()`', async () => { + const { opts, logs, blockTime } = await withdrawToAsync({ + toTokenAddress: wethTokenAddress, + }); + const exchangeAddress = await getExchangeForTokenAsync(opts.fromTokenAddress); + let calls: any = filterLogs(logs, ContractEvents.TokenToEthSwapInput); + expect(calls.length).to.eq(1); + expect(calls[0].args.exchange).to.eq(exchangeAddress); + expect(calls[0].args.tokensSold).to.bignumber.eq(opts.fromTokenBalance); + expect(calls[0].args.minEthBought).to.bignumber.eq(opts.amount); + expect(calls[0].args.deadline).to.bignumber.eq(blockTime); + calls = filterLogs( + logs.slice(calls[0].logIndex as number), + ContractEvents.WethDeposit, + ); + expect(calls.length).to.eq(1); + expect(calls[0].args.amount).to.bignumber.eq(opts.exchangeFillAmount); + calls = filterLogs( + logs.slice(calls[0].logIndex as number), + ContractEvents.TokenTransfer, + ); + expect(calls.length).to.eq(1); + expect(calls[0].args.token).to.eq(opts.toTokenAddress); + expect(calls[0].args.from).to.eq(testContract.address); + expect(calls[0].args.to).to.eq(opts.toAddress); + expect(calls[0].args.amount).to.bignumber.eq(opts.exchangeFillAmount); + }); + + it('sets allowance for "from" token', async () => { + const { opts, logs } = await withdrawToAsync({ + toTokenAddress: wethTokenAddress, + }); + const transfers = filterLogsToArguments(logs, ContractEvents.TokenApprove); + const exchangeAddress = await getExchangeForTokenAsync(opts.fromTokenAddress); + expect(transfers.length).to.eq(1); + expect(transfers[0].spender).to.eq(exchangeAddress); + expect(transfers[0].allowance).to.bignumber.eq(constants.MAX_UINT256); + }); + + it('sets allowance for "from" token on subsequent calls', async () => { + const { opts } = await withdrawToAsync({ + toTokenAddress: wethTokenAddress, + }); + const { logs } = await withdrawToAsync(opts); + const approvals = filterLogsToArguments(logs, ContractEvents.TokenApprove); + const exchangeAddress = await getExchangeForTokenAsync(opts.fromTokenAddress); + expect(approvals.length).to.eq(1); + expect(approvals[0].spender).to.eq(exchangeAddress); + expect(approvals[0].allowance).to.bignumber.eq(constants.MAX_UINT256); + }); + + it('fails if "from" token does not exist', async () => { + const tx = testContract.withdrawTo.awaitTransactionSuccessAsync( + randomAddress(), + randomAddress(), + randomAddress(), + getRandomInteger(1, 1e18), + hexLeftPad(wethTokenAddress), + ); + return expect(tx).to.revertWith('NO_UNISWAP_EXCHANGE_FOR_TOKEN'); + }); + + it('fails if `WETH.deposit()` fails', async () => { + const revertReason = 'FOOBAR'; + const tx = withdrawToAsync({ + toTokenAddress: wethTokenAddress, + toTokenRevertReason: revertReason, + }); + return expect(tx).to.revertWith(revertReason); + }); + + it('fails if the exchange fails', async () => { + const revertReason = 'FOOBAR'; + const tx = withdrawToAsync({ + toTokenAddress: wethTokenAddress, + exchangeRevertReason: revertReason, + }); + return expect(tx).to.revertWith(revertReason); + }); + }); + + describe('ETH -> token', () => { + it('calls `WETH.withdraw()`, then `IUniswapExchange.ethToTokenTransferInput()`', async () => { + const { opts, logs, blockTime } = await withdrawToAsync({ + fromTokenAddress: wethTokenAddress, + }); + const exchangeAddress = await getExchangeForTokenAsync(opts.toTokenAddress); + let calls: any = filterLogs(logs, ContractEvents.WethWithdraw); + expect(calls.length).to.eq(1); + expect(calls[0].args.amount).to.bignumber.eq(opts.fromTokenBalance); + calls = filterLogs( + logs.slice(calls[0].logIndex as number), + ContractEvents.EthToTokenTransferInput, + ); + expect(calls.length).to.eq(1); + expect(calls[0].args.exchange).to.eq(exchangeAddress); + expect(calls[0].args.minTokensBought).to.bignumber.eq(opts.amount); + expect(calls[0].args.deadline).to.bignumber.eq(blockTime); + expect(calls[0].args.recipient).to.eq(opts.toAddress); + }); + + it('does not set any allowance', async () => { + const { logs } = await withdrawToAsync({ + fromTokenAddress: wethTokenAddress, + }); + const approvals = filterLogsToArguments(logs, ContractEvents.TokenApprove); + expect(approvals).to.be.empty(''); + }); + + it('fails if "to" token does not exist', async () => { + const tx = testContract.withdrawTo.awaitTransactionSuccessAsync( + wethTokenAddress, + randomAddress(), + randomAddress(), + getRandomInteger(1, 1e18), + hexLeftPad(randomAddress()), + ); + return expect(tx).to.revertWith('NO_UNISWAP_EXCHANGE_FOR_TOKEN'); + }); + + it('fails if the `WETH.withdraw()` fails', async () => { + const revertReason = 'FOOBAR'; + const tx = withdrawToAsync({ + fromTokenAddress: wethTokenAddress, + fromTokenRevertReason: revertReason, + }); + return expect(tx).to.revertWith(revertReason); + }); + + it('fails if the exchange fails', async () => { + const revertReason = 'FOOBAR'; + const tx = withdrawToAsync({ + fromTokenAddress: wethTokenAddress, + exchangeRevertReason: revertReason, + }); + return expect(tx).to.revertWith(revertReason); + }); + }); + }); +}); diff --git a/contracts/asset-proxy/tsconfig.json b/contracts/asset-proxy/tsconfig.json index b3faf3e356..8ca5339b81 100644 --- a/contracts/asset-proxy/tsconfig.json +++ b/contracts/asset-proxy/tsconfig.json @@ -14,7 +14,8 @@ "generated-artifacts/IAuthorizable.json", "generated-artifacts/IERC20Bridge.json", "generated-artifacts/IEth2Dai.json", - "generated-artifacts/IWallet.json", + "generated-artifacts/IUniswapExchange.json", + "generated-artifacts/IUniswapExchangeFactory.json", "generated-artifacts/MixinAssetProxyDispatcher.json", "generated-artifacts/MixinAuthorizable.json", "generated-artifacts/MultiAssetProxy.json", @@ -22,7 +23,9 @@ "generated-artifacts/StaticCallProxy.json", "generated-artifacts/TestERC20Bridge.json", "generated-artifacts/TestEth2DaiBridge.json", - "generated-artifacts/TestStaticCallTarget.json" + "generated-artifacts/TestStaticCallTarget.json", + "generated-artifacts/TestUniswapBridge.json", + "generated-artifacts/UniswapBridge.json" ], "exclude": ["./deploy/solc/solc_bin"] } diff --git a/contracts/exchange-libs/CHANGELOG.json b/contracts/exchange-libs/CHANGELOG.json index 9fd46bc262..4059813c1e 100644 --- a/contracts/exchange-libs/CHANGELOG.json +++ b/contracts/exchange-libs/CHANGELOG.json @@ -105,6 +105,10 @@ { "note": "Update `IncompleteFillError` to take an `errorCode`, `expectedAssetFillAmount`, and `actualAssetFillAmount` fields.", "pr": 2075 + }, + { + "note": "Move `IWallet.sol` from `asset-proxy` and `exchange` packages to here.", + "pr": 2233 } ], "timestamp": 1570135330 diff --git a/contracts/asset-proxy/contracts/src/interfaces/IWallet.sol b/contracts/exchange-libs/contracts/src/IWallet.sol similarity index 100% rename from contracts/asset-proxy/contracts/src/interfaces/IWallet.sol rename to contracts/exchange-libs/contracts/src/IWallet.sol diff --git a/contracts/exchange-libs/package.json b/contracts/exchange-libs/package.json index b22d8ec706..e21a07ead7 100644 --- a/contracts/exchange-libs/package.json +++ b/contracts/exchange-libs/package.json @@ -35,7 +35,7 @@ "compile:truffle": "truffle compile" }, "config": { - "abis": "./generated-artifacts/@(LibEIP712ExchangeDomain|LibExchangeRichErrors|LibFillResults|LibMath|LibMathRichErrors|LibOrder|LibZeroExTransaction|TestLibEIP712ExchangeDomain|TestLibFillResults|TestLibMath|TestLibOrder|TestLibZeroExTransaction).json", + "abis": "./generated-artifacts/@(IWallet|LibEIP712ExchangeDomain|LibExchangeRichErrors|LibFillResults|LibMath|LibMathRichErrors|LibOrder|LibZeroExTransaction|TestLibEIP712ExchangeDomain|TestLibFillResults|TestLibMath|TestLibOrder|TestLibZeroExTransaction).json", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually." }, "repository": { diff --git a/contracts/exchange-libs/src/artifacts.ts b/contracts/exchange-libs/src/artifacts.ts index 7bfb67a9d3..1769fc432a 100644 --- a/contracts/exchange-libs/src/artifacts.ts +++ b/contracts/exchange-libs/src/artifacts.ts @@ -5,6 +5,7 @@ */ import { ContractArtifact } from 'ethereum-types'; +import * as IWallet from '../generated-artifacts/IWallet.json'; import * as LibEIP712ExchangeDomain from '../generated-artifacts/LibEIP712ExchangeDomain.json'; import * as LibExchangeRichErrors from '../generated-artifacts/LibExchangeRichErrors.json'; import * as LibFillResults from '../generated-artifacts/LibFillResults.json'; @@ -18,6 +19,7 @@ import * as TestLibMath from '../generated-artifacts/TestLibMath.json'; import * as TestLibOrder from '../generated-artifacts/TestLibOrder.json'; import * as TestLibZeroExTransaction from '../generated-artifacts/TestLibZeroExTransaction.json'; export const artifacts = { + IWallet: IWallet as ContractArtifact, LibEIP712ExchangeDomain: LibEIP712ExchangeDomain as ContractArtifact, LibExchangeRichErrors: LibExchangeRichErrors as ContractArtifact, LibFillResults: LibFillResults as ContractArtifact, diff --git a/contracts/exchange-libs/src/wrappers.ts b/contracts/exchange-libs/src/wrappers.ts index 89a7ffc03b..695fe2da79 100644 --- a/contracts/exchange-libs/src/wrappers.ts +++ b/contracts/exchange-libs/src/wrappers.ts @@ -3,6 +3,7 @@ * Warning: This file is auto-generated by contracts-gen. Don't edit manually. * ----------------------------------------------------------------------------- */ +export * from '../generated-wrappers/i_wallet'; export * from '../generated-wrappers/lib_e_i_p712_exchange_domain'; export * from '../generated-wrappers/lib_exchange_rich_errors'; export * from '../generated-wrappers/lib_fill_results'; diff --git a/contracts/exchange-libs/tsconfig.json b/contracts/exchange-libs/tsconfig.json index 7e46a4852c..b75670d9f7 100644 --- a/contracts/exchange-libs/tsconfig.json +++ b/contracts/exchange-libs/tsconfig.json @@ -3,6 +3,7 @@ "compilerOptions": { "outDir": "lib", "rootDir": ".", "resolveJsonModule": true }, "include": ["./src/**/*", "./test/**/*", "./scripts/**/*", "./generated-wrappers/**/*"], "files": [ + "generated-artifacts/IWallet.json", "generated-artifacts/LibEIP712ExchangeDomain.json", "generated-artifacts/LibExchangeRichErrors.json", "generated-artifacts/LibFillResults.json", diff --git a/packages/sol-compiler/src/cli.ts b/packages/sol-compiler/src/cli.ts index aa71246542..e53a680af2 100644 --- a/packages/sol-compiler/src/cli.ts +++ b/packages/sol-compiler/src/cli.ts @@ -40,6 +40,7 @@ const SEPARATOR = ','; contractsDir: argv.contractsDir, artifactsDir: argv.artifactsDir, contracts, + isOfflineMode: process.env.SOLC_OFFLINE ? true : undefined, }; const compiler = new Compiler(opts); if (argv.watch) { diff --git a/packages/sol-compiler/src/compiler.ts b/packages/sol-compiler/src/compiler.ts index 919f9909bc..1c16a72829 100644 --- a/packages/sol-compiler/src/compiler.ts +++ b/packages/sol-compiler/src/compiler.ts @@ -28,7 +28,10 @@ import { createDirIfDoesNotExistAsync, getContractArtifactIfExistsAsync, getDependencyNameToPackagePath, + getSolcJSAsync, + getSolcJSFromPath, getSolcJSReleasesAsync, + getSolcJSVersionFromPath, getSourcesWithDependencies, getSourceTreeHash, makeContractPathsRelative, @@ -106,7 +109,10 @@ export class Compiler { : {}; assert.doesConformToSchema('compiler.json', config, compilerOptionsSchema); this._contractsDir = path.resolve(passedOpts.contractsDir || config.contractsDir || DEFAULT_CONTRACTS_DIR); - this._solcVersionIfExists = passedOpts.solcVersion || config.solcVersion; + this._solcVersionIfExists = + process.env.SOLCJS_PATH !== undefined + ? getSolcJSVersionFromPath(process.env.SOLCJS_PATH) + : passedOpts.solcVersion || config.solcVersion; this._compilerSettings = { ...DEFAULT_COMPILER_SETTINGS, ...config.compilerSettings, @@ -292,7 +298,11 @@ export class Compiler { compilerOutput = await compileDockerAsync(solcVersion, input.standardInput); } else { fullSolcVersion = solcJSReleases[solcVersion]; - compilerOutput = await compileSolcJSAsync(solcVersion, input.standardInput, this._isOfflineMode); + const solcInstance = + process.env.SOLCJS_PATH !== undefined + ? getSolcJSFromPath(process.env.SOLCJS_PATH) + : await getSolcJSAsync(solcVersion, this._isOfflineMode); + compilerOutput = await compileSolcJSAsync(solcInstance, input.standardInput); } if (compilerOutput.errors !== undefined) { printCompilationErrorsAndWarnings(compilerOutput.errors); diff --git a/packages/sol-compiler/src/utils/compiler.ts b/packages/sol-compiler/src/utils/compiler.ts index 085e1a55f2..c5d96ca0cc 100644 --- a/packages/sol-compiler/src/utils/compiler.ts +++ b/packages/sol-compiler/src/utils/compiler.ts @@ -136,16 +136,14 @@ export async function getSolcJSReleasesAsync(isOfflineMode: boolean): Promise { - const solcInstance = await getSolcJSAsync(solcVersion, isOfflineMode); const standardInputStr = JSON.stringify(standardInput); const standardOutputStr = solcInstance.compileStandardWrapper(standardInputStr); const compiled: solc.StandardOutput = JSON.parse(standardOutputStr); @@ -364,6 +362,22 @@ export async function getSolcJSAsync(solcVersion: string, isOfflineMode: boolean return solcInstance; } +/** + * Gets the solidity compiler instance from a module path. + * @param path The path to the solc module. + */ +export function getSolcJSFromPath(modulePath: string): solc.SolcInstance { + return require(modulePath); +} + +/** + * Gets the solidity compiler version from a module path. + * @param path The path to the solc module. + */ +export function getSolcJSVersionFromPath(modulePath: string): string { + return require(modulePath).version(); +} + /** * Solidity compiler emits the bytecode without a 0x prefix for a hex. This function fixes it if bytecode is present. * @param compiledContract The standard JSON output section for a contract. Geth modified in place.