diff --git a/contracts/cfg/truffle-config.polygon.js b/contracts/cfg/truffle-config.polygon.js index d34342e..c1a12a9 100644 --- a/contracts/cfg/truffle-config.polygon.js +++ b/contracts/cfg/truffle-config.polygon.js @@ -1,7 +1,6 @@ -const HDWalletProvider = require('@truffle/hdwallet-provider'); +const HDWalletProvider = require("@truffle/hdwallet-provider"); - -require('dotenv').config({path:'.env'}); +require("dotenv").config({ path: ".env" }); /** * Use this file to configure your truffle project. It's seeded with some @@ -29,9 +28,9 @@ require('dotenv').config({path:'.env'}); // const mnemonic = fs.readFileSync('.secret').toString().trim(); module.exports = { - contracts_directory: './contracts', - contracts_build_directory: './build/contracts', - migrations_directory: './migrations/polygon', + contracts_directory: "./contracts", + contracts_build_directory: "./build/contracts", + migrations_directory: "./migrations/polygon", /** * Networks define how you connect to your ethereum client and let you set the * defaults web3 uses to send transactions. If you don't specify one truffle @@ -42,18 +41,18 @@ module.exports = { * $ truffle test --network */ - networks: { + networks: { // Useful for testing. The `development` name is special - truffle uses it by default // if it's defined here and no other network is specified at the command line. // You should run a client (like ganache-cli, geth or parity) in a separate terminal // tab if you use this network and you must also set the `host`, `port` and `network_id` // options below to some value. // - development: { - host: '127.0.0.1', // Localhost (default: none) - port: 8545, // Standard Ethereum port (default: none) - network_id: '*', // Any network (default: none) - }, + development: { + host: "127.0.0.1", // Localhost (default: none) + port: 8545, // Standard Ethereum port (default: none) + network_id: "*", // Any network (default: none) + }, // Another network with more advanced options... // advanced: { // port: 8777, // Custom port @@ -65,14 +64,15 @@ module.exports = { // }, // Useful for deploying to a public network. // NB: It's important to wrap the provider as a function. - mumbai: { - provider: () => new HDWalletProvider(process.env.ETH_PRIVATE_KEY, process.env.MUMBAI_PROVIDER), - network_id: 80001, - //gas: 4465030, - //confirmations: 2, // # of confs to wait between deployments. (default: 0) - //timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50) - //skipDryRun: true // Skip dry run before migrations? (default: false for public nets ) - }, + mumbai: { + provider: () => new HDWalletProvider(process.env.ETH_PRIVATE_KEY, process.env.MUMBAI_PROVIDER), + network_id: 80001, + gasPrice: 80000000000, + gas: 7000000, + //confirmations: 2, // # of confs to wait between deployments. (default: 0) + timeoutBlocks: 50, // # of blocks before a deployment times out (minimum/default: 50) + skipDryRun: true, // Skip dry run before migrations? (default: false for public nets ) + }, // Useful for private networks // private: { // provider: () => new HDWalletProvider(mnemonic, `https://network.io`), @@ -89,16 +89,16 @@ module.exports = { // Configure your compilers compilers: { solc: { - version: '0.7.6', // Fetch exact version from solc-bin (default: truffle's version) + version: "0.7.6", // Fetch exact version from solc-bin (default: truffle's version) // docker: true, // Use '0.5.1' you've installed locally with docker (default: false) // settings: { // See the solidity docs for advice about optimization and evmVersion optimizer: { - enabled: false, - runs: 200 + enabled: false, + runs: 200, }, // evmVersion: 'byzantium' // } - } + }, }, // Truffle DB is currently disabled by default; to enable it, change enabled: @@ -108,17 +108,17 @@ module.exports = { // NOTE: It is not possible to migrate your contracts to truffle DB and you should // make a backup of your artifacts to a safe location before enabling this feature. // - // After you backed up your artifacts you can utilize db by running migrate as follows: + // After you backed up your artifacts you can utilize db by running migrate as follows: // $ truffle migrate --reset --compile-all // // db: { - // enabled: false, - // host: '127.0.0.1', - // adapter: { - // name: 'sqlite', - // settings: { - // directory: '.db' - // } - // } + // enabled: false, + // host: '127.0.0.1', + // adapter: { + // name: 'sqlite', + // settings: { + // directory: '.db' + // } + // } // } }; diff --git a/contracts/cfg/truffle-config.tokens.js b/contracts/cfg/truffle-config.tokens.js new file mode 100644 index 0000000..3add8dc --- /dev/null +++ b/contracts/cfg/truffle-config.tokens.js @@ -0,0 +1,123 @@ +const HDWalletProvider = require("@truffle/hdwallet-provider"); + +require("dotenv").config({ path: ".env" }); + +/** + * Use this file to configure your truffle project. It's seeded with some + * common settings for different networks and features like migrations, + * compilation and testing. Uncomment the ones you need or modify + * them to suit your project as necessary. + * + * More information about configuration can be found at: + * + * trufflesuite.com/docs/advanced/configuration + * + * To deploy via Infura you'll need a wallet provider (like @truffle/hdwallet-provider) + * to sign your transactions before they're sent to a remote public node. Infura accounts + * are available for free at: infura.io/register. + * + * You'll also need a mnemonic - the twelve word phrase the wallet uses to generate + * public/private key pairs. If you're publishing your code to GitHub make sure you load this + * phrase from a file you've .gitignored so it doesn't accidentally become public. + * + */ + +// const HDWalletProvider = require('@truffle/hdwallet-provider'); +// +// const fs = require('fs'); +// const mnemonic = fs.readFileSync('.secret').toString().trim(); + +module.exports = { + contracts_directory: "./contracts", + contracts_build_directory: "./build/contracts", + migrations_directory: "./migrations/tokens", + /** + * Networks define how you connect to your ethereum client and let you set the + * defaults web3 uses to send transactions. If you don't specify one truffle + * will spin up a development blockchain for you on port 9545 when you + * run `develop` or `test`. You can ask a truffle command to use a specific + * network from the command line, e.g + * + * $ truffle test --network + */ + + networks: { + // Useful for testing. The `development` name is special - truffle uses it by default + // if it's defined here and no other network is specified at the command line. + // You should run a client (like ganache-cli, geth or parity) in a separate terminal + // tab if you use this network and you must also set the `host`, `port` and `network_id` + // options below to some value. + // + development: { + host: "127.0.0.1", // Localhost (default: none) + port: 8545, // Standard Ethereum port (default: none) + network_id: "*", // Any network (default: none) + }, + // Another network with more advanced options... + // advanced: { + // port: 8777, // Custom port + // network_id: 1342, // Custom network + // gas: 8500000, // Gas sent with each transaction (default: ~6700000) + // gasPrice: 20000000000, // 20 gwei (in wei) (default: 100 gwei) + // from:
, // Account to send txs from (default: accounts[0]) + // websocket: true // Enable EventEmitter interface for web3 (default: false) + // }, + // Useful for deploying to a public network. + // NB: It's important to wrap the provider as a function. + goerli: { + provider: () => new HDWalletProvider(process.env.ETH_PRIVATE_KEY, process.env.GOERLI_PROVIDER), + network_id: 5, + //gas: 4465030, + //confirmations: 2, // # of confs to wait between deployments. (default: 0) + //timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50) + //skipDryRun: true // Skip dry run before migrations? (default: false for public nets ) + }, + // Useful for private networks + // private: { + // provider: () => new HDWalletProvider(mnemonic, `https://network.io`), + // network_id: 2111, // This network is yours, in the cloud. + // production: true // Treats this network as if it was a public net. (default: false) + // } + }, + + // Set default mocha options here, use special reporters etc. + mocha: { + // timeout: 100000 + }, + + // Configure your compilers + compilers: { + solc: { + version: "0.7.6", // Fetch exact version from solc-bin (default: truffle's version) + // docker: true, // Use '0.5.1' you've installed locally with docker (default: false) + // settings: { // See the solidity docs for advice about optimization and evmVersion + optimizer: { + enabled: false, + runs: 200, + }, + // evmVersion: 'byzantium' + // } + }, + }, + + // Truffle DB is currently disabled by default; to enable it, change enabled: + // false to enabled: true. The default storage location can also be + // overridden by specifying the adapter settings, as shown in the commented code below. + // + // NOTE: It is not possible to migrate your contracts to truffle DB and you should + // make a backup of your artifacts to a safe location before enabling this feature. + // + // After you backed up your artifacts you can utilize db by running migrate as follows: + // $ truffle migrate --reset --compile-all + // + // db: { + // enabled: false, + // host: '127.0.0.1', + // adapter: { + // name: 'sqlite', + // settings: { + // directory: '.db' + // } + // } + // } +}; diff --git a/contracts/contracts/CrossChainSwapV2.sol b/contracts/contracts/CrossChainSwapV2.sol index 3689efa..e0591a5 100644 --- a/contracts/contracts/CrossChainSwapV2.sol +++ b/contracts/contracts/CrossChainSwapV2.sol @@ -3,8 +3,10 @@ pragma solidity ^0.7.6; pragma abicoder v2; -import './IWormhole.sol'; -import './SwapHelper.sol'; +import './shared/IWormhole.sol'; +import './shared/SwapHelper.sol'; +import './shared/TokenBridge.sol'; +import './shared/WETH.sol'; import 'solidity-bytes-utils/contracts/BytesLib.sol'; import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import '@openzeppelin/contracts/token/ERC20/SafeERC20.sol'; @@ -12,45 +14,18 @@ import '@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol'; import "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol"; -interface TokenBridge { - function transferTokensWithPayload( - address token, - uint256 amount, - uint16 recipientChain, - bytes32 recipient, - uint256 arbiterFee, - uint32 nonce, - bytes memory payload - ) external payable returns (uint64); - function completeTransferWithPayload( - bytes memory encodedVm, - address feeRecipient - ) external returns (bytes memory); -} - - -interface IWETH is IERC20 { - function deposit() external payable; - function withdraw(uint amount) external; -} - - /// @title A cross-chain UniswapV2 example /// @notice Swaps against UniswapV2 pools and uses Wormhole TokenBridge /// for cross-chain transfers contract CrossChainSwapV2 { using SafeERC20 for IERC20; using BytesLib for bytes; - uint8 public immutable typeExactIn = 1; - uint8 public immutable typeExactOut = 2; - uint8 public immutable typeNativeSwap = 1; - uint8 public immutable typeTokenSwap = 2; - uint16 public immutable expectedVaaLength = 274; - uint8 public immutable terraChainId = 3; - IUniswapV2Router02 public immutable swapRouter; - address public immutable feeTokenAddress; - address public immutable tokenBridgeAddress; - address public immutable wrappedNative; + uint8 public immutable TypeExactIn = 1; + uint8 public immutable TypeExactOut = 2; + IUniswapV2Router02 public immutable SWAP_ROUTER; + address public immutable FEE_TOKEN_ADDRESS; + address public immutable TOKEN_BRIDGE_ADDRESS; + address public immutable WRAPPED_NATIVE; constructor( address _swapRouterAddress, @@ -58,10 +33,10 @@ contract CrossChainSwapV2 { address _tokenBridgeAddress, address _wrappedNativeAddress ) { - swapRouter = IUniswapV2Router02(_swapRouterAddress); - feeTokenAddress = _feeTokenAddress; - tokenBridgeAddress = _tokenBridgeAddress; - wrappedNative = _wrappedNativeAddress; + SWAP_ROUTER = IUniswapV2Router02(_swapRouterAddress); + FEE_TOKEN_ADDRESS = _feeTokenAddress; + TOKEN_BRIDGE_ADDRESS = _tokenBridgeAddress; + WRAPPED_NATIVE = _wrappedNativeAddress; } /// @dev Used to communicate information about executed swaps to UI/user @@ -77,20 +52,12 @@ contract CrossChainSwapV2 { /// instructions after redeeming the VAA from the TokenBridge function _getParsedPayload( bytes calldata encodedVaa, - uint8 swapFunctionType, - uint8 swapCurrencyType, - address feeRecipient + uint8 swapFunctionType ) private returns (SwapHelper.DecodedVaaParameters memory payload) { // complete the transfer on the token bridge bytes memory vmPayload = TokenBridge( - tokenBridgeAddress - ).completeTransferWithPayload(encodedVaa, feeRecipient); - - // make sure payload is the right size - require( - vmPayload.length==expectedVaaLength, - "VAA has the wrong number of bytes" - ); + TOKEN_BRIDGE_ADDRESS + ).completeTransferWithPayload(encodedVaa); // parse the payload payload = SwapHelper.decodeVaaPayload(vmPayload); @@ -100,42 +67,19 @@ contract CrossChainSwapV2 { payload.swapFunctionType==swapFunctionType, "incorrect swapFunctionType in payload" ); - require( - payload.swapCurrencyType==swapCurrencyType, - "incorrect swapCurrencyType in payload" - ); } /// @dev Executes exactIn native asset swap and pays the relayer function recvAndSwapExactNativeIn( bytes calldata encodedVaa ) external payable returns (uint256[] memory amounts) { - // check token balance before redeeming the payload - (,bytes memory queriedBalanceBefore) = feeTokenAddress.staticcall( - abi.encodeWithSelector(IERC20.balanceOf.selector, - address(this) - )); - uint256 balanceBefore = abi.decode(queriedBalanceBefore, (uint256)); - // redeem and fetch parsed payload SwapHelper.DecodedVaaParameters memory payload = _getParsedPayload( encodedVaa, - typeExactIn, - typeNativeSwap, - msg.sender // feeRecipient + TypeExactIn ); - // query token balance after redeeming the payload - (,bytes memory queriedBalanceAfter) = feeTokenAddress.staticcall( - abi.encodeWithSelector(IERC20.balanceOf.selector, - address(this) - )); - uint256 balanceAfter = abi.decode(queriedBalanceAfter, (uint256)); - - // the balance change is the swap amount (less relayer fees) - uint256 swapAmountLessFees = balanceAfter - balanceBefore; - // create dynamic address array // uniswap won't take fixed size array address[] memory uniPath = new address[](2); @@ -144,32 +88,39 @@ contract CrossChainSwapV2 { // sanity check path require( - uniPath[0]==feeTokenAddress, - "tokenIn must be UST" + uniPath[0]==FEE_TOKEN_ADDRESS, + "tokenIn must be feeToken" ); require( - uniPath[1]==wrappedNative, + uniPath[1]==WRAPPED_NATIVE, "tokenOut must be wrapped native asset" ); // approve the router to spend tokens TransferHelper.safeApprove( uniPath[0], - address(swapRouter), - swapAmountLessFees + address(SWAP_ROUTER), + payload.swapAmount ); // try to execute the swap - try swapRouter.swapExactTokensForTokens( - swapAmountLessFees, + try SWAP_ROUTER.swapExactTokensForTokens( + payload.swapAmount, payload.estimatedAmount, uniPath, address(this), payload.deadline ) returns (uint256[] memory amounts) { + // calculate how much to pay the relayer in the native token + uint256 nativeRelayerFee = amounts[1] * payload.relayerFee / payload.swapAmount; + uint256 nativeAmountOut = amounts[1] - nativeRelayerFee; + // unwrap native and send to recipient - IWETH(wrappedNative).withdraw(amounts[1]); - payable(payload.recipientAddress).transfer(amounts[1]); + IWETH(WRAPPED_NATIVE).withdraw(amounts[1]); + payable(payload.recipientAddress).transfer(nativeAmountOut); + + /// pay the relayer in the native token + payable(msg.sender).transfer(nativeRelayerFee); // used in UI to tell user they're getting // their desired token @@ -177,84 +128,104 @@ contract CrossChainSwapV2 { payload.recipientAddress, uniPath[1], msg.sender, - amounts[1], + nativeAmountOut, 1 ); return amounts; } catch { - // swap failed - return UST to recipient - IERC20(feeTokenAddress).safeTransfer( + // pay relayer in the feeToken since the swap failed + IERC20 feeToken = IERC20(FEE_TOKEN_ADDRESS); + feeToken.safeTransfer(msg.sender, payload.relayerFee); + + // swap failed - return feeToken to recipient + feeToken.safeTransfer( payload.recipientAddress, - swapAmountLessFees + payload.swapAmount - payload.relayerFee ); // used in UI to tell user they're getting - // UST instead of their desired native asset + // feeToken instead of their desired native asset emit SwapResult( payload.recipientAddress, uniPath[0], msg.sender, - swapAmountLessFees, + payload.swapAmount - payload.relayerFee, 0 ); } } - /// @dev Executes exactIn token swap and pays the relayer - function recvAndSwapExactIn( + /// @dev Executes exactOut native asset swap and pays the relayer + function recvAndSwapExactNativeOut( bytes calldata encodedVaa - ) external returns (uint256[] memory amounts) { - // check token balance before redeeming the payload - (,bytes memory queriedBalanceBefore) = feeTokenAddress.staticcall( - abi.encodeWithSelector(IERC20.balanceOf.selector, - address(this) - )); - uint256 balanceBefore = abi.decode(queriedBalanceBefore, (uint256)); - - // redeem and fetch the parsed payload + ) external returns (uint256 amountInUsed) { + // redeem and fetch parsed payload SwapHelper.DecodedVaaParameters memory payload = _getParsedPayload( encodedVaa, - typeExactIn, - typeTokenSwap, - msg.sender // feeRecipient + TypeExactOut ); - // query token balance after redeeming the payload - (,bytes memory queriedBalanceAfter) = feeTokenAddress.staticcall( - abi.encodeWithSelector(IERC20.balanceOf.selector, - address(this) - )); - uint256 balanceAfter = abi.decode(queriedBalanceAfter, (uint256)); - - // the balance change is the swap amount (less relayer fees) - uint256 swapAmountLessFees = balanceAfter - balanceBefore; - // create dynamic address array - uniswap won't take fixed size array address[] memory uniPath = new address[](2); uniPath[0] = payload.path[0]; uniPath[1] = payload.path[1]; - // make sure first element in path is UST - require(uniPath[0]==feeTokenAddress, "tokenIn must be UST"); + // sanity check path + require( + uniPath[0]==FEE_TOKEN_ADDRESS, + "tokenIn must be feeToken" + ); + require( + payload.path[1]==WRAPPED_NATIVE, + "tokenOut must be wrapped native asset" + ); + + // pay the relayer in feeToken so that user gets desired exact amount out + IERC20 feeToken = IERC20(FEE_TOKEN_ADDRESS); + feeToken.safeTransfer(msg.sender, payload.relayerFee); + uint256 maxAmountInLessFees = payload.swapAmount - payload.relayerFee; + + // amountOut is the estimated swap amount for exact out methods + uint256 amountOut = payload.estimatedAmount; // approve the router to spend tokens TransferHelper.safeApprove( uniPath[0], - address(swapRouter), - swapAmountLessFees - ); + address(SWAP_ROUTER), + maxAmountInLessFees + ); - // try to perform the swap - try swapRouter.swapExactTokensForTokens( - swapAmountLessFees, - payload.estimatedAmount, + // try to perform the swap + try SWAP_ROUTER.swapTokensForExactTokens( + amountOut, + maxAmountInLessFees, uniPath, - payload.recipientAddress, + address(this), payload.deadline ) returns (uint256[] memory amounts) { + // amountIn used is first element in array + amountInUsed = amounts[0]; + + // refund recipient with any feeToken not used in the swap + if (amountInUsed < maxAmountInLessFees) { + TransferHelper.safeApprove( + FEE_TOKEN_ADDRESS, + address(SWAP_ROUTER), + 0 + ); + IERC20(FEE_TOKEN_ADDRESS).safeTransfer( + payload.recipientAddress, + maxAmountInLessFees - amountInUsed + ); + } + + // unwrap native and send to recipient + IWETH(WRAPPED_NATIVE).withdraw(amounts[1]); + payable(payload.recipientAddress).transfer(amounts[1]); + // used in UI to tell user they're getting - // their desired token + // their desired native asset emit SwapResult( payload.recipientAddress, uniPath[1], @@ -262,28 +233,28 @@ contract CrossChainSwapV2 { amounts[1], 1 ); - return amounts; + return amountInUsed; } catch { - // swap failed - return UST to recipient - IERC20(feeTokenAddress).safeTransfer( + // swap failed - return feeToken to recipient + IERC20(FEE_TOKEN_ADDRESS).safeTransfer( payload.recipientAddress, - swapAmountLessFees + maxAmountInLessFees ); // used in UI to tell user they're getting - // UST instead of their desired token + // feeToken instead of their desired native asset emit SwapResult( payload.recipientAddress, uniPath[0], msg.sender, - swapAmountLessFees, + maxAmountInLessFees, 0 ); - } + } } - /// @dev Executes exactIn native asset and token swaps before - /// sending a custom payload to the TokenBridge + + /// @dev Executes exactIn native asset swap function _swapExactInBeforeTransfer( uint256 amountIn, uint256 amountOutMinimum, @@ -294,12 +265,12 @@ contract CrossChainSwapV2 { // approve the router to spend tokens TransferHelper.safeApprove( path[0], - address(swapRouter), + address(SWAP_ROUTER), amountIn ); // perform the swap - uint256[] memory amounts = swapRouter.swapExactTokensForTokens( + uint256[] memory amounts = SWAP_ROUTER.swapExactTokensForTokens( amountIn, amountOutMinimum, path, @@ -309,69 +280,6 @@ contract CrossChainSwapV2 { amountOut = amounts[1]; } - /// @dev Calls _swapExactInBeforeTransfer and encodes custom payload with - /// instructions for executing token swaps on the destination chain - function swapExactInAndTransfer( - SwapHelper.ExactInParameters calldata swapParams, - address[] calldata path, - uint256 relayerFee, - uint16 targetChainId, - bytes32 targetContractAddress, - uint32 nonce - ) external { - require( - swapParams.amountOutMinimum > relayerFee, - "insufficient amountOutMinimum to pay relayer" - ); - require( - path[1]==feeTokenAddress, - "tokenOut must be UST for first swap" - ); - - // send tokens to this contract - IERC20 token = IERC20(path[0]); - token.safeTransferFrom(msg.sender, address(this), swapParams.amountIn); - - // peform the first swap - uint256 amountOut = _swapExactInBeforeTransfer( - swapParams.amountIn, - swapParams.amountOutMinimum, - msg.sender, - path[0:2], - swapParams.deadline - ); - - // encode payload for second swap - bytes memory payload = abi.encodePacked( - swapParams.targetAmountOutMinimum, - swapParams.targetChainRecipient, - path[2], - path[3], - swapParams.deadline, - swapParams.poolFee, - typeExactIn, - typeTokenSwap - ); - - // approve token bridge to spend feeTokens (UST) - TransferHelper.safeApprove( - feeTokenAddress, - tokenBridgeAddress, - amountOut - ); - - // send transfer with payload to the TokenBridge - TokenBridge(tokenBridgeAddress).transferTokensWithPayload( - feeTokenAddress, - amountOut, - targetChainId, - targetContractAddress, - relayerFee, - nonce, - payload - ); - } - /// @dev Calls _swapExactInBeforeTransfer and encodes custom payload with /// instructions for executing native asset swaps on the destination chain function swapExactNativeInAndTransfer( @@ -387,17 +295,17 @@ contract CrossChainSwapV2 { "insufficient amountOutMinimum to pay relayer" ); require( - path[0]==wrappedNative, + path[0]==WRAPPED_NATIVE, "tokenIn must be wrapped native asset for first swap" ); require( - path[1]==feeTokenAddress, - "tokenOut must be UST for first swap" + path[1]==FEE_TOKEN_ADDRESS, + "tokenOut must be feeToken for first swap" ); require(msg.value > 0, "must pass non 0 native asset amount"); // wrap native asset - IWETH(wrappedNative).deposit{ + IWETH(WRAPPED_NATIVE).deposit{ value : msg.value }(); @@ -410,276 +318,53 @@ contract CrossChainSwapV2 { swapParams.deadline ); - // create payload variable - bytes memory payload; - - // UST is native to Terra - no need for swap instructions - if (targetChainId == terraChainId) { - payload = abi.encodePacked( - swapParams.targetChainRecipient - ); - } else { - payload = abi.encodePacked( - swapParams.targetAmountOutMinimum, - swapParams.targetChainRecipient, - path[2], - path[3], - swapParams.deadline, - swapParams.poolFee, - typeExactIn, - typeNativeSwap - ); - } - - // approve token bridge to spend feeTokens (UST) + // create payload with target swap instructions + bytes memory payload = abi.encodePacked( + swapParams.targetAmountOutMinimum, + swapParams.targetChainRecipient, + path[2], + path[3], + swapParams.deadline, + swapParams.poolFee, + TypeExactIn, + relayerFee + ); + + // approve token bridge to spend feeTokens TransferHelper.safeApprove( - feeTokenAddress, - tokenBridgeAddress, + FEE_TOKEN_ADDRESS, + TOKEN_BRIDGE_ADDRESS, amountOut ); // send transfer with payload to the TokenBridge - TokenBridge(tokenBridgeAddress).transferTokensWithPayload( - feeTokenAddress, + TokenBridge(TOKEN_BRIDGE_ADDRESS).transferTokensWithPayload( + FEE_TOKEN_ADDRESS, amountOut, targetChainId, - targetContractAddress, - relayerFee, + targetContractAddress, nonce, payload ); } - - /// @dev Executes exactOut native asset swap and pays the relayer - function recvAndSwapExactNativeOut( - bytes calldata encodedVaa - ) external returns (uint256 amountInUsed) { - // check token balance before redeeming the payload - (,bytes memory queriedBalanceBefore) = feeTokenAddress.staticcall( - abi.encodeWithSelector(IERC20.balanceOf.selector, - address(this) - )); - uint256 balanceBefore = abi.decode(queriedBalanceBefore, (uint256)); - - // redeem and fetch parsed payload - SwapHelper.DecodedVaaParameters memory payload = - _getParsedPayload( - encodedVaa, - typeExactOut, - typeNativeSwap, - msg.sender // feeRecipient - ); - - // query token balance after redeeming the payload - (,bytes memory queriedBalanceAfter) = feeTokenAddress.staticcall( - abi.encodeWithSelector(IERC20.balanceOf.selector, - address(this) - )); - uint256 balanceAfter = abi.decode(queriedBalanceAfter, (uint256)); - - // the balance change is the swap amount (less relayer fees) - uint256 maxAmountInLessFees = balanceAfter - balanceBefore; - - // amountOut is the estimated swap amount for exact out methods - uint256 amountOut = payload.estimatedAmount; - - // create dynamic address array - uniswap won't take fixed size array - address[] memory uniPath = new address[](2); - uniPath[0] = payload.path[0]; - uniPath[1] = payload.path[1]; - - // sanity check path - require( - uniPath[0]==feeTokenAddress, - "tokenIn must be UST" - ); - require( - payload.path[1]==wrappedNative, - "tokenOut must be wrapped native asset" - ); - - // approve the router to spend tokens - TransferHelper.safeApprove( - uniPath[0], - address(swapRouter), - maxAmountInLessFees - ); - - // try to perform the swap - try swapRouter.swapTokensForExactTokens( - amountOut, - maxAmountInLessFees, - uniPath, - address(this), - payload.deadline - ) returns (uint256[] memory amounts) { - // amountIn used is first element in array - amountInUsed = amounts[0]; - - // refund recipient with any UST not used in the swap - if (amountInUsed < maxAmountInLessFees) { - TransferHelper.safeApprove( - feeTokenAddress, - address(swapRouter), - 0 - ); - IERC20(feeTokenAddress).safeTransfer( - payload.recipientAddress, - maxAmountInLessFees - amountInUsed - ); - } - - // unwrap native and send to recipient - IWETH(wrappedNative).withdraw(amounts[1]); - payable(payload.recipientAddress).transfer(amounts[1]); - - // used in UI to tell user they're getting - // their desired native asset - emit SwapResult( - payload.recipientAddress, - uniPath[1], - msg.sender, - amounts[1], - 1 - ); - return amountInUsed; - } catch { - // swap failed - return UST to recipient - IERC20(feeTokenAddress).safeTransfer( - payload.recipientAddress, - maxAmountInLessFees - ); - - // used in UI to tell user they're getting - // UST instead of their desired native asset - emit SwapResult( - payload.recipientAddress, - uniPath[0], - msg.sender, - maxAmountInLessFees, - 0 - ); - } - } - - /// @dev Executes exactOut token swap and pays the relayer - function recvAndSwapExactOut( - bytes calldata encodedVaa - ) external returns (uint256 amountInUsed) { - // check token balance before redeeming the payload - (,bytes memory queriedBalanceBefore) = feeTokenAddress.staticcall( - abi.encodeWithSelector(IERC20.balanceOf.selector, - address(this) - )); - uint256 balanceBefore = abi.decode(queriedBalanceBefore, (uint256)); - - // redeem and fetch parsed payload - SwapHelper.DecodedVaaParameters memory payload = - _getParsedPayload( - encodedVaa, - typeExactOut, - typeTokenSwap, - msg.sender // feeRecipient - ); - - // query token balance after redeeming the payload - (,bytes memory queriedBalanceAfter) = feeTokenAddress.staticcall( - abi.encodeWithSelector(IERC20.balanceOf.selector, - address(this) - )); - uint256 balanceAfter = abi.decode(queriedBalanceAfter, (uint256)); - - // the balance change is the swap amount (less relayer fees) - uint256 maxAmountInLessFees = balanceAfter - balanceBefore; - - // amountOut is the estimated swap amount for exact out methods - uint256 amountOut = payload.estimatedAmount; - - // create dynamic address array - uniswap won't take fixed size array - address[] memory uniPath = new address[](2); - uniPath[0] = payload.path[0]; - uniPath[1] = payload.path[1]; - require(uniPath[0]==feeTokenAddress, "tokenIn must be UST"); - - // approve the router to spend tokens - TransferHelper.safeApprove( - uniPath[0], - address(swapRouter), - maxAmountInLessFees - ); - - // try to perform the swap - try swapRouter.swapTokensForExactTokens( - amountOut, - maxAmountInLessFees, - uniPath, - payload.recipientAddress, - payload.deadline - ) returns (uint256[] memory amounts) { - // amountIn used is first element in array - amountInUsed = amounts[0]; - - // refund recipient with any UST not used in the swap - if (amountInUsed < maxAmountInLessFees) { - TransferHelper.safeApprove( - feeTokenAddress, - address(swapRouter), - 0 - ); - IERC20(feeTokenAddress).safeTransfer( - payload.recipientAddress, - maxAmountInLessFees - amountInUsed - ); - } - - // used in UI to tell user they're getting - // their desired token - emit SwapResult( - payload.recipientAddress, - uniPath[1], - msg.sender, - amounts[1], - 1 - ); - return amountInUsed; - } catch { - // swap failed - return UST to recipient - IERC20(feeTokenAddress).safeTransfer( - payload.recipientAddress, - maxAmountInLessFees - ); - - // used in UI to tell user they're getting - // UST instead of their desired token - emit SwapResult( - payload.recipientAddress, - uniPath[0], - msg.sender, - maxAmountInLessFees, - 0 - ); - } - } - - /// @dev Executes exactOut native asset and token swaps before - /// sending a custom payload to the TokenBridge + + /// @dev Executes exactOut native asset swaps function _swapExactOutBeforeTransfer( uint256 amountOut, uint256 amountInMaximum, address contractCaller, address[] calldata path, - uint256 deadline, - uint8 swapType + uint256 deadline ) internal { // approve the router to spend tokens TransferHelper.safeApprove( path[0], - address(swapRouter), + address(SWAP_ROUTER), amountInMaximum ); // perform the swap - uint256[] memory amounts = swapRouter.swapTokensForExactTokens( + uint256[] memory amounts = SWAP_ROUTER.swapTokensForExactTokens( amountOut, amountInMaximum, path, @@ -692,94 +377,17 @@ contract CrossChainSwapV2 { // refund contractCaller with any amountIn that wasn't spent if (amountInUsed < amountInMaximum) { - TransferHelper.safeApprove(path[0], address(swapRouter), 0); - if (swapType == typeTokenSwap) { - // send remaining tokens to contractCaller - IERC20 token = IERC20(path[0]); - token.safeTransfer( - contractCaller, - amountInMaximum - amountInUsed - ); - } else { - // unwrap remaining native asset and send to contractCaller - IWETH(wrappedNative).withdraw( - amountInMaximum - amountInUsed - ); - payable(contractCaller).transfer( - amountInMaximum - amountInUsed - ); - } + // unwrap remaining native asset and send to contractCaller + TransferHelper.safeApprove(path[0], address(SWAP_ROUTER), 0); + IWETH(WRAPPED_NATIVE).withdraw( + amountInMaximum - amountInUsed + ); + payable(contractCaller).transfer( + amountInMaximum - amountInUsed + ); } } - /// @dev Calls _swapExactOutBeforeTransfer and encodes custom payload with - /// instructions for executing token swaps on the destination chain - function swapExactOutAndTransfer( - SwapHelper.ExactOutParameters calldata swapParams, - address[] calldata path, - uint256 relayerFee, - uint16 targetChainId, - bytes32 targetContractAddress, - uint32 nonce - ) external { - require( - swapParams.amountOut > relayerFee, - "insufficient amountOut to pay relayer" - ); - require( - path[1]==feeTokenAddress, - "tokenOut must be UST for first swap" - ); - - // send tokens to this contract before swap - IERC20 token = IERC20(path[0]); - token.safeTransferFrom( - msg.sender, - address(this), - swapParams.amountInMaximum - ); - - // peform the first swap - _swapExactOutBeforeTransfer( - swapParams.amountOut, - swapParams.amountInMaximum, - msg.sender, - path[0:2], - swapParams.deadline, - typeTokenSwap - ); - - // encode payload for second swap - bytes memory payload = abi.encodePacked( - swapParams.targetAmountOut, - swapParams.targetChainRecipient, - path[2], - path[3], - swapParams.deadline, - swapParams.poolFee, - typeExactOut, - typeTokenSwap - ); - - // approve token bridge to spend feeTokens (UST) - TransferHelper.safeApprove( - feeTokenAddress, - tokenBridgeAddress, - swapParams.amountOut - ); - - // send transfer with payload to the TokenBridge - TokenBridge(tokenBridgeAddress).transferTokensWithPayload( - feeTokenAddress, - swapParams.amountOut, - targetChainId, - targetContractAddress, - relayerFee, - nonce, - payload - ); - } - /// @dev Calls _swapExactOutBeforeTransfer and encodes custom payload with /// instructions for executing native asset swaps on the destination chain function swapExactNativeOutAndTransfer( @@ -795,17 +403,17 @@ contract CrossChainSwapV2 { "insufficient amountOut to pay relayer" ); require( - path[0]==wrappedNative, + path[0]==WRAPPED_NATIVE, "tokenIn must be wrapped native asset for first swap" ); require( - path[1]==feeTokenAddress, - "tokenOut must be UST for first swap" + path[1]==FEE_TOKEN_ADDRESS, + "tokenOut must be feeToken for first swap" ); require(msg.value > 0, "must pass non 0 native asset amount"); // wrap native asset - IWETH(wrappedNative).deposit{ + IWETH(WRAPPED_NATIVE).deposit{ value : msg.value }(); @@ -815,45 +423,34 @@ contract CrossChainSwapV2 { msg.value, msg.sender, path[0:2], - swapParams.deadline, - typeNativeSwap + swapParams.deadline ); - // create payload variable - bytes memory payload; + // create payload with target swap instructions + bytes memory payload = abi.encodePacked( + swapParams.targetAmountOut, + swapParams.targetChainRecipient, + path[2], + path[3], + swapParams.deadline, + swapParams.poolFee, + TypeExactOut, + relayerFee + ); - // UST is native to Terra - no need for swap instructions - if (targetChainId == terraChainId) { - payload = abi.encodePacked( - swapParams.targetChainRecipient - ); - } else { - payload = abi.encodePacked( - swapParams.targetAmountOut, - swapParams.targetChainRecipient, - path[2], - path[3], - swapParams.deadline, - swapParams.poolFee, - typeExactOut, - typeNativeSwap - ); - } - - // approve token bridge to spend feeTokens (UST) + // approve token bridge to spend feeTokens TransferHelper.safeApprove( - feeTokenAddress, - tokenBridgeAddress, + FEE_TOKEN_ADDRESS, + TOKEN_BRIDGE_ADDRESS, swapParams.amountOut ); // send transfer with payload to the TokenBridge - TokenBridge(tokenBridgeAddress).transferTokensWithPayload( - feeTokenAddress, + TokenBridge(TOKEN_BRIDGE_ADDRESS).transferTokensWithPayload( + FEE_TOKEN_ADDRESS, swapParams.amountOut, targetChainId, targetContractAddress, - relayerFee, nonce, payload ); diff --git a/contracts/contracts/CrossChainSwapV3.sol b/contracts/contracts/CrossChainSwapV3.sol index 5ca0368..af5202b 100644 --- a/contracts/contracts/CrossChainSwapV3.sol +++ b/contracts/contracts/CrossChainSwapV3.sol @@ -3,8 +3,10 @@ pragma solidity ^0.7.6; pragma abicoder v2; -import './IWormhole.sol'; -import './SwapHelper.sol'; +import './shared/IWormhole.sol'; +import './shared/SwapHelper.sol'; +import './shared/TokenBridge.sol'; +import './shared/WETH.sol'; import 'solidity-bytes-utils/contracts/BytesLib.sol'; import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import '@openzeppelin/contracts/token/ERC20/SafeERC20.sol'; @@ -12,28 +14,6 @@ import '@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol'; import '@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol'; -interface TokenBridge { - function transferTokensWithPayload( - address token, - uint256 amount, - uint16 recipientChain, - bytes32 recipient, - uint256 arbiterFee, - uint32 nonce, - bytes memory payload - ) external payable returns (uint64); - function completeTransferWithPayload( - bytes memory encodedVm, - address feeRecipient - ) external returns (bytes memory); -} - - -interface IWETH is IERC20 { - function withdraw(uint amount) external; -} - - interface IUniswapRouter is ISwapRouter { function refundETH() external payable; } @@ -45,16 +25,12 @@ interface IUniswapRouter is ISwapRouter { contract CrossChainSwapV3 { using SafeERC20 for IERC20; using BytesLib for bytes; - uint8 public immutable typeExactIn = 1; - uint8 public immutable typeExactOut = 2; - uint8 public immutable typeNativeSwap = 1; - uint8 public immutable typeTokenSwap = 2; - uint16 public immutable expectedVaaLength = 274; - uint8 public immutable terraChainId = 3; - IUniswapRouter public immutable swapRouter; - address public immutable feeTokenAddress; - address public immutable tokenBridgeAddress; - address public immutable wrappedNative; + uint8 public immutable TypeExactIn = 1; + uint8 public immutable TypeExactOut = 2; + IUniswapRouter public immutable SWAP_ROUTER; + address public immutable FEE_TOKEN_ADDRESS; + address public immutable TOKEN_BRIDGE_ADDRESS; + address public immutable WRAPPED_NATIVE; constructor( address _swapRouterAddress, @@ -62,10 +38,10 @@ contract CrossChainSwapV3 { address _tokenBridgeAddress, address _wrappedNativeAddress ) { - swapRouter = IUniswapRouter(_swapRouterAddress); - feeTokenAddress = _feeTokenAddress; - tokenBridgeAddress = _tokenBridgeAddress; - wrappedNative = _wrappedNativeAddress; + SWAP_ROUTER = IUniswapRouter(_swapRouterAddress); + FEE_TOKEN_ADDRESS = _feeTokenAddress; + TOKEN_BRIDGE_ADDRESS = _tokenBridgeAddress; + WRAPPED_NATIVE = _wrappedNativeAddress; } /// @dev Used to communicate information about executed swaps to UI/user @@ -78,23 +54,15 @@ contract CrossChainSwapV3 { ); /// @dev Returns the parsed TokenBridge payload which contains swap - /// instructions after redeeming from the VAA from the TokenBridge + /// instructions after redeeming the VAA from the TokenBridge function _getParsedPayload( bytes calldata encodedVaa, - uint8 swapFunctionType, - uint8 swapCurrencyType, - address feeRecipient + uint8 swapFunctionType ) private returns (SwapHelper.DecodedVaaParameters memory payload) { // complete the transfer on the token bridge bytes memory vmPayload = TokenBridge( - tokenBridgeAddress - ).completeTransferWithPayload(encodedVaa, feeRecipient); - - // make sure payload is the right size - require( - vmPayload.length==expectedVaaLength, - "VAA has the wrong number of bytes" - ); + TOKEN_BRIDGE_ADDRESS + ).completeTransferWithPayload(encodedVaa); // parse the payload payload = SwapHelper.decodeVaaPayload(vmPayload); @@ -104,56 +72,34 @@ contract CrossChainSwapV3 { payload.swapFunctionType==swapFunctionType, "incorrect swapFunctionType in payload" ); - require( - payload.swapCurrencyType==swapCurrencyType, - "incorrect swapCurrencyType in payload" - ); - } + } /// @dev Executes exactIn native asset swap and pays the relayer function recvAndSwapExactNativeIn( bytes calldata encodedVaa ) external returns (uint256 amountOut) { - // check token balance before redeeming the payload - (,bytes memory queriedBalanceBefore) = feeTokenAddress.staticcall( - abi.encodeWithSelector(IERC20.balanceOf.selector, - address(this) - )); - uint256 balanceBefore = abi.decode(queriedBalanceBefore, (uint256)); - + // redeem and fetch parsed payload SwapHelper.DecodedVaaParameters memory payload = _getParsedPayload( encodedVaa, - typeExactIn, - typeNativeSwap, - msg.sender // feeRecipient - ); - - // query token balance after redeeming the payload - (,bytes memory queriedBalanceAfter) = feeTokenAddress.staticcall( - abi.encodeWithSelector(IERC20.balanceOf.selector, - address(this) - )); - uint256 balanceAfter = abi.decode(queriedBalanceAfter, (uint256)); - - // the balance change is the swap amount (less relayer fees) - uint256 swapAmountLessFees = balanceAfter - balanceBefore; + TypeExactIn + ); // sanity check path require( - payload.path[0]==feeTokenAddress, - "tokenIn must be UST" + payload.path[0]==FEE_TOKEN_ADDRESS, + "tokenIn must be feeToken" ); require( - payload.path[1]==wrappedNative, + payload.path[1]==WRAPPED_NATIVE, "tokenOut must be wrapped Native" - ); + ); // approve the router to spend tokens TransferHelper.safeApprove( payload.path[0], - address(swapRouter), - swapAmountLessFees + address(SWAP_ROUTER), + payload.swapAmount ); // set swap options with user params @@ -164,16 +110,23 @@ contract CrossChainSwapV3 { fee: payload.poolFee, recipient: address(this), deadline: payload.deadline, - amountIn: swapAmountLessFees, + amountIn: payload.swapAmount, amountOutMinimum: payload.estimatedAmount, sqrtPriceLimitX96: 0 }); // try to execute the swap - try swapRouter.exactInputSingle(params) returns (uint256 amountOut) { + try SWAP_ROUTER.exactInputSingle(params) returns (uint256 amountOut) { + // calculate how much to pay the relayer in the native token + uint256 nativeRelayerFee = amountOut * payload.relayerFee / payload.swapAmount; + uint256 nativeAmountOut = amountOut - nativeRelayerFee; + // unwrap native and send to recipient - IWETH(wrappedNative).withdraw(amountOut); - payable(payload.recipientAddress).transfer(amountOut); + IWETH(WRAPPED_NATIVE).withdraw(amountOut); + payable(payload.recipientAddress).transfer(nativeAmountOut); + + /// pay the relayer in the native token + payable(msg.sender).transfer(nativeRelayerFee); // used in UI to tell user they're getting // their desired token @@ -181,86 +134,103 @@ contract CrossChainSwapV3 { payload.recipientAddress, payload.path[1], msg.sender, - amountOut, + nativeAmountOut, 1 ); return amountOut; } catch { - // swap failed - return UST to recipient - IERC20(feeTokenAddress).safeTransfer( + // pay relayer in the feeToken since the swap failed + IERC20 feeToken = IERC20(FEE_TOKEN_ADDRESS); + feeToken.safeTransfer(msg.sender, payload.relayerFee); + + // swap failed - return feeToken (less relayer fees) to recipient + feeToken.safeTransfer( payload.recipientAddress, - swapAmountLessFees + payload.swapAmount - payload.relayerFee ); // used in UI to tell user they're getting - // UST instead of their desired native asset + // feeToken instead of their desired native asset emit SwapResult( payload.recipientAddress, payload.path[0], msg.sender, - swapAmountLessFees, + payload.swapAmount - payload.relayerFee, 0 ); } } - /// @dev Executes exactIn token swap and pays the relayer - function recvAndSwapExactIn( + /// @dev Executes exactOut native asset swap and pays the relayer + function recvAndSwapExactNativeOut( bytes calldata encodedVaa - ) external returns (uint256 amountOut) { - // check token balance before redeeming the payload - (,bytes memory queriedBalanceBefore) = feeTokenAddress.staticcall( - abi.encodeWithSelector(IERC20.balanceOf.selector, - address(this) - )); - uint256 balanceBefore = abi.decode(queriedBalanceBefore, (uint256)); - - // redeem and fetch the parsed payload + ) external returns (uint256 amountInUsed) { + // redeem and fetch parsed payload SwapHelper.DecodedVaaParameters memory payload = _getParsedPayload( encodedVaa, - typeExactIn, - typeTokenSwap, - msg.sender // feeRecipient + TypeExactOut ); - // query token balance after redeeming the payload - (,bytes memory queriedBalanceAfter) = feeTokenAddress.staticcall( - abi.encodeWithSelector(IERC20.balanceOf.selector, - address(this) - )); - uint256 balanceAfter = abi.decode(queriedBalanceAfter, (uint256)); + // sanity check path + require( + payload.path[0]==FEE_TOKEN_ADDRESS, + "tokenIn must be feeToken" + ); + require( + payload.path[1]==WRAPPED_NATIVE, + "tokenOut must be wrapped native asset" + ); - // the balance change is the swap amount (less relayer fees) - uint256 swapAmountLessFees = balanceAfter - balanceBefore; + // pay the relayer in feeToken so that user gets desired exact amount out + IERC20 feeToken = IERC20(FEE_TOKEN_ADDRESS); + feeToken.safeTransfer(msg.sender, payload.relayerFee); + uint256 maxAmountInLessFees = payload.swapAmount - payload.relayerFee; - // check path to see if first element is the feeToken - require(payload.path[0]==feeTokenAddress, "tokenIn must be UST"); + // amountOut is the estimated swap amount for exact out methods + uint256 amountOut = payload.estimatedAmount; // approve the router to spend tokens TransferHelper.safeApprove( payload.path[0], - address(swapRouter), - swapAmountLessFees - ); - + address(SWAP_ROUTER), + maxAmountInLessFees + ); + // set swap options with user params - ISwapRouter.ExactInputSingleParams memory params = - ISwapRouter.ExactInputSingleParams({ + ISwapRouter.ExactOutputSingleParams memory params = + ISwapRouter.ExactOutputSingleParams({ tokenIn: payload.path[0], tokenOut: payload.path[1], fee: payload.poolFee, - recipient: payload.recipientAddress, + recipient: address(this), deadline: payload.deadline, - amountIn: swapAmountLessFees, - amountOutMinimum: payload.estimatedAmount, + amountOut: amountOut, + amountInMaximum: maxAmountInLessFees, sqrtPriceLimitX96: 0 }); // try to perform the swap - try swapRouter.exactInputSingle(params) returns (uint256 amountOut) { + try SWAP_ROUTER.exactOutputSingle(params) returns (uint256 amountInUsed) { + // refund recipient with any feeToken not used in the swap + if (amountInUsed < maxAmountInLessFees) { + TransferHelper.safeApprove( + FEE_TOKEN_ADDRESS, + address(SWAP_ROUTER), + 0 + ); + IERC20(FEE_TOKEN_ADDRESS).safeTransfer( + payload.recipientAddress, + maxAmountInLessFees - amountInUsed + ); + } + + // unwrap native and send to recipient + IWETH(WRAPPED_NATIVE).withdraw(amountOut); + payable(payload.recipientAddress).transfer(amountOut); + // used in UI to tell user they're getting - // their desired token + // their desired native asset emit SwapResult( payload.recipientAddress, payload.path[1], @@ -268,50 +238,35 @@ contract CrossChainSwapV3 { amountOut, 1 ); - return amountOut; + return amountInUsed; } catch { - // swap failed - return UST to recipient - IERC20(feeTokenAddress).safeTransfer( + // swap failed - return feeToken to recipient + IERC20(FEE_TOKEN_ADDRESS).safeTransfer( payload.recipientAddress, - swapAmountLessFees + maxAmountInLessFees ); // used in UI to tell user they're getting - // UST instead of their desired token + // feeToken instead of their desired native asset emit SwapResult( payload.recipientAddress, payload.path[0], msg.sender, - swapAmountLessFees, + maxAmountInLessFees, 0 ); } } - /// @dev Executes exactIn native asset and token swaps before - /// sending a custom payload to the TokenBridge + /// @dev Executes exactIn native asset swap function _swapExactInBeforeTransfer( uint256 amountIn, uint256 amountOutMinimum, address contractCaller, address[] calldata path, uint256 deadline, - uint24 poolFee, - uint8 swapType + uint24 poolFee ) internal returns (uint256 amountOut) { - if (swapType == typeTokenSwap) { - // transfer the allowed amount of tokens to this contract - IERC20 token = IERC20(path[0]); - token.safeTransferFrom(contractCaller, address(this), amountIn); - - // approve the router to spend tokens - TransferHelper.safeApprove( - path[0], - address(swapRouter), - amountIn - ); - } - // set swap options with user params ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({ @@ -326,74 +281,9 @@ contract CrossChainSwapV3 { }); // perform the swap - if (swapType == typeTokenSwap) { - amountOut = swapRouter.exactInputSingle(params); - } else { // native swap - amountOut = swapRouter.exactInputSingle{value: amountIn}(params); - } + amountOut = SWAP_ROUTER.exactInputSingle{value: amountIn}(params); } - /// @dev Calls _swapExactInBeforeTransfer and encodes custom payload with - /// instructions for executing token swaps on the destination chain - function swapExactInAndTransfer( - SwapHelper.ExactInParameters calldata swapParams, - address[] calldata path, - uint256 relayerFee, - uint16 targetChainId, - bytes32 targetContractAddress, - uint32 nonce - ) external { - require( - swapParams.amountOutMinimum > relayerFee, - "insufficient amountOutMinimum to pay relayer" - ); - require( - path[1]==feeTokenAddress, - "tokenOut must be UST for first swap" - ); - - // peform the first swap - uint256 amountOut = _swapExactInBeforeTransfer( - swapParams.amountIn, - swapParams.amountOutMinimum, - msg.sender, - path[0:2], - swapParams.deadline, - swapParams.poolFee, - typeTokenSwap - ); - - // encode payload for second swap - bytes memory payload = abi.encodePacked( - swapParams.targetAmountOutMinimum, - swapParams.targetChainRecipient, - path[2], - path[3], - swapParams.deadline, - swapParams.poolFee, - typeExactIn, - typeTokenSwap - ); - - // approve token bridge to spend feeTokens (UST) - TransferHelper.safeApprove( - feeTokenAddress, - tokenBridgeAddress, - amountOut - ); - - // send transfer with payload to the TokenBridge - TokenBridge(tokenBridgeAddress).transferTokensWithPayload( - feeTokenAddress, - amountOut, - targetChainId, - targetContractAddress, - relayerFee, - nonce, - payload - ); - } - /// @dev Calls _swapExactInBeforeTransfer and encodes custom payload with /// instructions for executing native asset swaps on the destination chain function swapExactNativeInAndTransfer( @@ -409,12 +299,12 @@ contract CrossChainSwapV3 { "insufficient amountOutMinimum to pay relayer" ); require( - path[0]==wrappedNative, + path[0]==WRAPPED_NATIVE, "tokenIn must be wrapped native asset for first swap" ); require( - path[1]==feeTokenAddress, - "tokenOut must be UST for first swap" + path[1]==FEE_TOKEN_ADDRESS, + "tokenOut must be feeToken for first swap" ); require(msg.value > 0, "must pass non 0 native asset amount"); @@ -425,291 +315,48 @@ contract CrossChainSwapV3 { msg.sender, path[0:2], swapParams.deadline, - swapParams.poolFee, - typeNativeSwap + swapParams.poolFee ); - // create payload variable - bytes memory payload; + // create payload with target swap instructions + bytes memory payload = abi.encodePacked( + swapParams.targetAmountOutMinimum, + swapParams.targetChainRecipient, + path[2], + path[3], + swapParams.deadline, + swapParams.poolFee, + TypeExactIn, + relayerFee + ); - // UST is native to Terra - no need for swap instructions - if (targetChainId == terraChainId) { - payload = abi.encodePacked( - swapParams.targetChainRecipient - ); - } else { - payload = abi.encodePacked( - swapParams.targetAmountOutMinimum, - swapParams.targetChainRecipient, - path[2], - path[3], - swapParams.deadline, - swapParams.poolFee, - typeExactIn, - typeNativeSwap - ); - } - - // approve token bridge to spend feeTokens (UST) + // approve token bridge to spend feeTokens TransferHelper.safeApprove( - feeTokenAddress, - tokenBridgeAddress, + FEE_TOKEN_ADDRESS, + TOKEN_BRIDGE_ADDRESS, amountOut ); // send transfer with payload to the TokenBridge - TokenBridge(tokenBridgeAddress).transferTokensWithPayload( - feeTokenAddress, + TokenBridge(TOKEN_BRIDGE_ADDRESS).transferTokensWithPayload( + FEE_TOKEN_ADDRESS, amountOut, targetChainId, - targetContractAddress, - relayerFee, + targetContractAddress, nonce, payload ); - } + } - /// @dev Executes exactOut native asset swap and pays the relayer - function recvAndSwapExactNativeOut( - bytes calldata encodedVaa - ) external returns (uint256 amountInUsed) { - // check token balance before redeeming the payload - (,bytes memory queriedBalanceBefore) = feeTokenAddress.staticcall( - abi.encodeWithSelector(IERC20.balanceOf.selector, - address(this) - )); - uint256 balanceBefore = abi.decode(queriedBalanceBefore, (uint256)); - - // redeem and fetch parsed payload - SwapHelper.DecodedVaaParameters memory payload = - _getParsedPayload( - encodedVaa, - typeExactOut, - typeNativeSwap, - msg.sender // feeRecipient - ); - - // query token balance after redeeming the payload - (,bytes memory queriedBalanceAfter) = feeTokenAddress.staticcall( - abi.encodeWithSelector(IERC20.balanceOf.selector, - address(this) - )); - uint256 balanceAfter = abi.decode(queriedBalanceAfter, (uint256)); - - // the balance change is the swap amount (less relayer fees) - uint256 maxAmountInLessFees = balanceAfter - balanceBefore; - - // sanity check path - require( - payload.path[0]==feeTokenAddress, - "tokenIn must be UST" - ); - require( - payload.path[1]==wrappedNative, - "tokenOut must be wrapped native asset" - ); - - // amountOut is the estimated swap amount for exact out methods - uint256 amountOut = payload.estimatedAmount; - - // approve the router to spend tokens - TransferHelper.safeApprove( - payload.path[0], - address(swapRouter), - maxAmountInLessFees - ); - - // set swap options with user params - ISwapRouter.ExactOutputSingleParams memory params = - ISwapRouter.ExactOutputSingleParams({ - tokenIn: payload.path[0], - tokenOut: payload.path[1], - fee: payload.poolFee, - recipient: address(this), - deadline: payload.deadline, - amountOut: amountOut, - amountInMaximum: maxAmountInLessFees, - sqrtPriceLimitX96: 0 - }); - - // try to perform the swap - try swapRouter.exactOutputSingle(params) returns (uint256 amountInUsed) { - // refund recipient with any UST not used in the swap - if (amountInUsed < maxAmountInLessFees) { - TransferHelper.safeApprove( - feeTokenAddress, - address(swapRouter), - 0 - ); - IERC20(feeTokenAddress).safeTransfer( - payload.recipientAddress, - maxAmountInLessFees - amountInUsed - ); - } - - // unwrap native and send to recipient - IWETH(wrappedNative).withdraw(amountOut); - payable(payload.recipientAddress).transfer(amountOut); - - // used in UI to tell user they're getting - // their desired native asset - emit SwapResult( - payload.recipientAddress, - payload.path[1], - msg.sender, - amountOut, - 1 - ); - return amountInUsed; - } catch { - // swap failed - return UST to recipient - IERC20(feeTokenAddress).safeTransfer( - payload.recipientAddress, - maxAmountInLessFees - ); - - // used in UI to tell user they're getting - // UST instead of their desired native asset - emit SwapResult( - payload.recipientAddress, - payload.path[0], - msg.sender, - maxAmountInLessFees, - 0 - ); - } - } - - /// @dev Executes exactOut token swap and pays the relayer - function recvAndSwapExactOut( - bytes calldata encodedVaa - ) external returns (uint256 amountInUsed) { - // check token balance before redeeming the payload - (,bytes memory queriedBalanceBefore) = feeTokenAddress.staticcall( - abi.encodeWithSelector(IERC20.balanceOf.selector, - address(this) - )); - uint256 balanceBefore = abi.decode(queriedBalanceBefore, (uint256)); - - // redeem and fetch parsed payload - SwapHelper.DecodedVaaParameters memory payload = - _getParsedPayload( - encodedVaa, - typeExactOut, - typeTokenSwap, - msg.sender // feeRecipient - ); - - // query token balance after redeeming the payload - (,bytes memory queriedBalanceAfter) = feeTokenAddress.staticcall( - abi.encodeWithSelector(IERC20.balanceOf.selector, - address(this) - )); - uint256 balanceAfter = abi.decode(queriedBalanceAfter, (uint256)); - - // the balance change is the swap amount (less relayer fees) - uint256 maxAmountInLessFees = balanceAfter - balanceBefore; - - // check path to see if first element is the feeToken - require(payload.path[0]==feeTokenAddress, "tokenIn must be UST"); - - // amountOut is the estimated swap amount for exact out methods - uint256 amountOut = payload.estimatedAmount; - - // approve the router to spend tokens - TransferHelper.safeApprove( - payload.path[0], - address(swapRouter), - maxAmountInLessFees - ); - - // set swap options with user params - ISwapRouter.ExactOutputSingleParams memory params = - ISwapRouter.ExactOutputSingleParams({ - tokenIn: payload.path[0], - tokenOut: payload.path[1], - fee: payload.poolFee, - recipient: payload.recipientAddress, - deadline: payload.deadline, - amountOut: amountOut, - amountInMaximum: maxAmountInLessFees, - sqrtPriceLimitX96: 0 - }); - - // try to perform the swap - try swapRouter.exactOutputSingle(params) returns (uint256 amountInUsed) { - // refund recipient with any UST not used in the swap - if (amountInUsed < maxAmountInLessFees) { - TransferHelper.safeApprove( - feeTokenAddress, - address(swapRouter), - 0 - ); - IERC20(feeTokenAddress).safeTransfer( - payload.recipientAddress, - maxAmountInLessFees - amountInUsed - ); - } - - // used in UI to tell user they're getting - // their desired token - emit SwapResult( - payload.recipientAddress, - payload.path[1], - msg.sender, - amountOut, - 1 - ); - return amountInUsed; - } catch { - // swap failed - return UST to recipient - IERC20(feeTokenAddress).safeTransfer( - payload.recipientAddress, - maxAmountInLessFees - ); - - // used in UI to tell user they're getting - // UST instead of their desired token - emit SwapResult( - payload.recipientAddress, - payload.path[0], - msg.sender, - maxAmountInLessFees, - 0 - ); - } - } - - /// @dev Executes exactOut native asset and token swaps before - /// sending a custom payload to the TokenBridge + /// @dev Executes exactOut native asset swaps function _swapExactOutBeforeTransfer( uint256 amountOut, uint256 amountInMaximum, address contractCaller, address[] calldata path, uint256 deadline, - uint24 poolFee, - uint8 swapType - ) internal { - // create instance of erc20 token for token swaps - IERC20 token = IERC20(path[0]); - - if (swapType == typeTokenSwap) { - // transfer tokens to this contract - token.safeTransferFrom( - contractCaller, - address(this), - amountInMaximum - ); - - // approve the router to spend tokens - TransferHelper.safeApprove( - path[0], - address(swapRouter), - amountInMaximum - ); - } - + uint24 poolFee + ) internal { // set swap options with user params ISwapRouter.ExactOutputSingleParams memory params = ISwapRouter.ExactOutputSingleParams({ @@ -723,95 +370,21 @@ contract CrossChainSwapV3 { sqrtPriceLimitX96: 0 }); - if (swapType == typeTokenSwap) { - // executes the swap returning the amountInUsed - uint256 amountInUsed = swapRouter.exactOutputSingle(params); + // executes the swap returning the amountInUsed + // ask for our money back -_- after the swap executes + uint256 amountInUsed = SWAP_ROUTER.exactOutputSingle{value: amountInMaximum}(params); + SWAP_ROUTER.refundETH(); - // return any amountIn not used in the swap to contractCaller - if (amountInUsed < amountInMaximum) { - TransferHelper.safeApprove(path[0], address(swapRouter), 0); - token.safeTransfer( - contractCaller, - amountInMaximum - amountInUsed - ); - } - } else { // native swap - // executes the swap returning the amountInUsed - // ask for our money back -_- after the swap executes - uint256 amountInUsed = swapRouter.exactOutputSingle{value: amountInMaximum}(params); - swapRouter.refundETH(); - - // return unused native asset to contractCaller - if (amountInUsed < amountInMaximum) { - TransferHelper.safeApprove(path[0], address(swapRouter), 0); - payable(contractCaller).transfer( - amountInMaximum - amountInUsed - ); - } + // return unused native asset to contractCaller + if (amountInUsed < amountInMaximum) { + // set SWAP_ROUTER allowance to zero + TransferHelper.safeApprove(path[0], address(SWAP_ROUTER), 0); + payable(contractCaller).transfer( + amountInMaximum - amountInUsed + ); } } - /// @dev Calls _swapExactOutBeforeTransfer and encodes custom payload with - /// instructions for executing token swaps on the destination chain - function swapExactOutAndTransfer( - SwapHelper.ExactOutParameters calldata swapParams, - address[] calldata path, - uint256 relayerFee, - uint16 targetChainId, - bytes32 targetContractAddress, - uint32 nonce - ) external { - require( - swapParams.amountOut > relayerFee, - "insufficient amountOut to pay relayer" - ); - require( - path[1]==feeTokenAddress, - "tokenOut must be UST for first swap" - ); - - // peform the first swap - _swapExactOutBeforeTransfer( - swapParams.amountOut, - swapParams.amountInMaximum, - msg.sender, - path[0:2], - swapParams.deadline, - swapParams.poolFee, - typeTokenSwap - ); - - // encode payload for second swap - bytes memory payload = abi.encodePacked( - swapParams.targetAmountOut, - swapParams.targetChainRecipient, - path[2], - path[3], - swapParams.deadline, - swapParams.poolFee, - typeExactOut, - typeTokenSwap - ); - - // approve token bridge to spend feeTokens (UST) - TransferHelper.safeApprove( - feeTokenAddress, - tokenBridgeAddress, - swapParams.amountOut - ); - - // send transfer with payload to the TokenBridge - TokenBridge(tokenBridgeAddress).transferTokensWithPayload( - feeTokenAddress, - swapParams.amountOut, - targetChainId, - targetContractAddress, - relayerFee, - nonce, - payload - ); - } - /// @dev Calls _swapExactOutBeforeTransfer and encodes custom payload with /// instructions for executing native asset swaps on the destination chain function swapExactNativeOutAndTransfer( @@ -827,12 +400,12 @@ contract CrossChainSwapV3 { "insufficient amountOut to pay relayer" ); require( - path[0]==wrappedNative, + path[0]==WRAPPED_NATIVE, "tokenIn must be wrapped native asset for first swap" ); require( - path[1]==feeTokenAddress, - "tokenOut must be UST for first swap" + path[1]==FEE_TOKEN_ADDRESS, + "tokenOut must be feeToken for first swap" ); require(msg.value > 0, "must pass non 0 native asset amount"); @@ -843,45 +416,34 @@ contract CrossChainSwapV3 { msg.sender, path[0:2], swapParams.deadline, - swapParams.poolFee, - typeNativeSwap + swapParams.poolFee ); - // create payload variable - bytes memory payload; + // create payload with target swap instructions + bytes memory payload = abi.encodePacked( + swapParams.targetAmountOut, + swapParams.targetChainRecipient, + path[2], + path[3], + swapParams.deadline, + swapParams.poolFee, + TypeExactOut, + relayerFee + ); - // UST is native to Terra - no need for swap instructions - if (targetChainId == terraChainId) { - payload = abi.encodePacked( - swapParams.targetChainRecipient - ); - } else { - payload = abi.encodePacked( - swapParams.targetAmountOut, - swapParams.targetChainRecipient, - path[2], - path[3], - swapParams.deadline, - swapParams.poolFee, - typeExactOut, - typeNativeSwap - ); - } - - // approve token bridge to spend feeTokens (UST) + // approve token bridge to spend feeTokens TransferHelper.safeApprove( - feeTokenAddress, - tokenBridgeAddress, + FEE_TOKEN_ADDRESS, + TOKEN_BRIDGE_ADDRESS, swapParams.amountOut ); // send transfer with payload to the TokenBridge - TokenBridge(tokenBridgeAddress).transferTokensWithPayload( - feeTokenAddress, + TokenBridge(TOKEN_BRIDGE_ADDRESS).transferTokensWithPayload( + FEE_TOKEN_ADDRESS, swapParams.amountOut, targetChainId, targetContractAddress, - relayerFee, nonce, payload ); diff --git a/contracts/contracts/IWormhole.sol b/contracts/contracts/shared/IWormhole.sol similarity index 100% rename from contracts/contracts/IWormhole.sol rename to contracts/contracts/shared/IWormhole.sol diff --git a/contracts/contracts/Structs.sol b/contracts/contracts/shared/Structs.sol similarity index 100% rename from contracts/contracts/Structs.sol rename to contracts/contracts/shared/Structs.sol diff --git a/contracts/contracts/SwapHelper.sol b/contracts/contracts/shared/SwapHelper.sol similarity index 92% rename from contracts/contracts/SwapHelper.sol rename to contracts/contracts/shared/SwapHelper.sol index 4b41f72..1d4d670 100644 --- a/contracts/contracts/SwapHelper.sol +++ b/contracts/contracts/shared/SwapHelper.sol @@ -6,12 +6,11 @@ pragma abicoder v2; import './IWormhole.sol'; import 'solidity-bytes-utils/contracts/BytesLib.sol'; - /// @title Helper library for cross-chain swaps /// @notice Contains functions necessary for parsing encoded VAAs /// and structs containing swap parameters library SwapHelper { - using BytesLib for bytes; + using BytesLib for bytes; /// @dev Parameters needed for exactIn swap type struct ExactInParameters { @@ -40,14 +39,14 @@ library SwapHelper { uint8 version; uint256 swapAmount; address contractAddress; - uint256 relayerFee; + bytes32 fromAddress; uint256 estimatedAmount; address recipientAddress; address[2] path; uint256 deadline; uint24 poolFee; uint8 swapFunctionType; - uint8 swapCurrencyType; + uint256 relayerFee; } /// @dev Decodes parameters encoded in a VAA @@ -71,7 +70,7 @@ library SwapHelper { // skip index += 2; - decoded.relayerFee = vmPayload.toUint256(index); + decoded.fromAddress = vmPayload.toBytes32(index); index += 32; decoded.estimatedAmount = vmPayload.toUint256(index); @@ -98,6 +97,9 @@ library SwapHelper { decoded.swapFunctionType = vmPayload.toUint8(index); index += 1; - decoded.swapCurrencyType = vmPayload.toUint8(index); + decoded.relayerFee = vmPayload.toUint256(index); + index += 32; + + require(vmPayload.length == index, "invalid payload length"); } } \ No newline at end of file diff --git a/contracts/contracts/shared/TokenBridge.sol b/contracts/contracts/shared/TokenBridge.sol new file mode 100644 index 0000000..ea2b373 --- /dev/null +++ b/contracts/contracts/shared/TokenBridge.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.7.6; +pragma abicoder v2; + +interface TokenBridge { + function transferTokensWithPayload( + address token, + uint256 amount, + uint16 recipientChain, + bytes32 recipient, + uint32 nonce, + bytes memory payload + ) external payable returns (uint64); + function completeTransferWithPayload( + bytes memory encodedVm + ) external returns (bytes memory); +} \ No newline at end of file diff --git a/contracts/contracts/shared/WETH.sol b/contracts/contracts/shared/WETH.sol new file mode 100644 index 0000000..4785797 --- /dev/null +++ b/contracts/contracts/shared/WETH.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.7.6; +pragma abicoder v2; + +import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; + +interface IWETH is IERC20 { + function deposit() external payable; + function withdraw(uint amount) external; +} \ No newline at end of file diff --git a/contracts/contracts/tokenImplementations/WormUSD.sol b/contracts/contracts/tokenImplementations/WormUSD.sol new file mode 100644 index 0000000..d382f91 --- /dev/null +++ b/contracts/contracts/tokenImplementations/WormUSD.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.7.6; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract WormUSD is ERC20 { + constructor(address mintToAddress, uint8 decimals, uint256 supply) ERC20("wormUSD", "WUSD"){ + _setupDecimals(decimals); + _mint(mintToAddress, supply*10**decimals); + } +} \ No newline at end of file diff --git a/contracts/deploy_token.sh b/contracts/deploy_token.sh new file mode 100755 index 0000000..d542515 --- /dev/null +++ b/contracts/deploy_token.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -euo pipefail + +npx truffle migrate --config cfg/truffle-config.tokens.js --network goerli --reset diff --git a/contracts/migrations/avalanche/2_deploy_contracts.js b/contracts/migrations/avalanche/2_deploy_contracts.js index 09d6a26..4a1bbf9 100644 --- a/contracts/migrations/avalanche/2_deploy_contracts.js +++ b/contracts/migrations/avalanche/2_deploy_contracts.js @@ -5,29 +5,23 @@ const SwapHelper = artifacts.require("SwapHelper"); const scriptsAddressPath = "../react/src/addresses"; -module.exports = async function(deployer, network) { - const routerAddress = "0x7e3411b04766089cfaa52db688855356a12f05d1"; // hurricaneswap - const feeTokenAddress = "0xe09ed38e5cd1014444846f62376ac88c5232cde9"; // wUST - const tokenBridgeAddress = "0x61E44E506Ca5659E6c0bba9b678586fA2d729756"; - const wrappedAvaxAddress = "0x1d308089a2d1ced3f1ce36b1fcaf815b07217be3"; +module.exports = async function (deployer, network) { + const routerAddress = "0x7e3411b04766089cfaa52db688855356a12f05d1"; // hurricaneswap router + const feeTokenAddress = "0x8F23C5BE43FBfE7a30c04e4eDEE3D18995Fe7E2d"; // wormUSD + const tokenBridgeAddress = "0x61E44E506Ca5659E6c0bba9b678586fA2d729756"; + const wrappedAvaxAddress = "0x1d308089a2d1ced3f1ce36b1fcaf815b07217be3"; - await deployer.deploy(SwapHelper); - await deployer.link(SwapHelper, CrossChainSwapV2); - await deployer.deploy( - CrossChainSwapV2, - routerAddress, - feeTokenAddress, - tokenBridgeAddress, - wrappedAvaxAddress - ); - - // save the contract address somewhere - await fsp.mkdir(scriptsAddressPath, { recursive: true }); + await deployer.deploy(SwapHelper); + await deployer.link(SwapHelper, CrossChainSwapV2); + await deployer.deploy(CrossChainSwapV2, routerAddress, feeTokenAddress, tokenBridgeAddress, wrappedAvaxAddress); - await fsp.writeFile( - `${scriptsAddressPath}/${network}.ts`, - `export const SWAP_CONTRACT_ADDRESS = '${CrossChainSwapV2.address}';` - ); + // save the contract address somewhere + await fsp.mkdir(scriptsAddressPath, { recursive: true }); + + await fsp.writeFile( + `${scriptsAddressPath}/${network}.ts`, + `export const SWAP_CONTRACT_ADDRESS = '${CrossChainSwapV2.address}';` + ); //deployer.link(ConvertLib, MetaCoin); //deployer.deploy(MetaCoin); diff --git a/contracts/migrations/bsc/2_deploy_contracts.js b/contracts/migrations/bsc/2_deploy_contracts.js index 5219f85..d2b570f 100644 --- a/contracts/migrations/bsc/2_deploy_contracts.js +++ b/contracts/migrations/bsc/2_deploy_contracts.js @@ -5,29 +5,23 @@ const SwapHelper = artifacts.require("SwapHelper"); const scriptsAddressPath = "../react/src/addresses"; -module.exports = async function(deployer, network) { - const routerAddress = "0x9Ac64Cc6e4415144C455BD8E4837Fea55603e5c3"; // pancakeswap - const feeTokenAddress = "0x7b8eae1e85c8b189ee653d3f78733f4f788bb2c1"; // wUST - const tokenBridgeAddress = "0x9dcF9D205C9De35334D646BeE44b2D2859712A09"; - const wrappedBnbAddress = "0xae13d989dac2f0debff460ac112a837c89baa7cd"; +module.exports = async function (deployer, network) { + const routerAddress = "0x9Ac64Cc6e4415144C455BD8E4837Fea55603e5c3"; // pancakeswap + const feeTokenAddress = "0x2A29D46D7e0B997358E9726DA0210Af212f2dfd7"; // wormUSD + const tokenBridgeAddress = "0x9dcF9D205C9De35334D646BeE44b2D2859712A09"; + const wrappedBnbAddress = "0xae13d989dac2f0debff460ac112a837c89baa7cd"; - await deployer.deploy(SwapHelper); - await deployer.link(SwapHelper, CrossChainSwapV2); - await deployer.deploy( - CrossChainSwapV2, - routerAddress, - feeTokenAddress, - tokenBridgeAddress, - wrappedBnbAddress - ); - - // save the contract address somewhere - await fsp.mkdir(scriptsAddressPath, { recursive: true }); + await deployer.deploy(SwapHelper); + await deployer.link(SwapHelper, CrossChainSwapV2); + await deployer.deploy(CrossChainSwapV2, routerAddress, feeTokenAddress, tokenBridgeAddress, wrappedBnbAddress); - await fsp.writeFile( - `${scriptsAddressPath}/${network}.ts`, - `export const SWAP_CONTRACT_ADDRESS = '${CrossChainSwapV2.address}';` - ); + // save the contract address somewhere + await fsp.mkdir(scriptsAddressPath, { recursive: true }); + + await fsp.writeFile( + `${scriptsAddressPath}/${network}.ts`, + `export const SWAP_CONTRACT_ADDRESS = '${CrossChainSwapV2.address}';` + ); //deployer.link(ConvertLib, MetaCoin); //deployer.deploy(MetaCoin); diff --git a/contracts/migrations/ethereum/2_deploy_contracts.js b/contracts/migrations/ethereum/2_deploy_contracts.js index 251a0b3..86d9d73 100644 --- a/contracts/migrations/ethereum/2_deploy_contracts.js +++ b/contracts/migrations/ethereum/2_deploy_contracts.js @@ -7,19 +7,13 @@ const scriptsAddressPath = "../react/src/addresses"; module.exports = async function (deployer, network) { const routerAddress = "0xE592427A0AEce92De3Edee1F18E0157C05861564"; - const feeTokenAddress = "0x36Ed51Afc79619b299b238898E72ce482600568a"; // wUST + const feeTokenAddress = "0x6336c2dA64408Fcc277e0E1104aC6c34c431464c"; // wormUSD const tokenBridgeAddress = "0xF890982f9310df57d00f659cf4fd87e65adEd8d7"; const wrappedEthAddress = "0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6"; await deployer.deploy(SwapHelper); await deployer.link(SwapHelper, CrossChainSwapV3); - await deployer.deploy( - CrossChainSwapV3, - routerAddress, - feeTokenAddress, - tokenBridgeAddress, - wrappedEthAddress - ); + await deployer.deploy(CrossChainSwapV3, routerAddress, feeTokenAddress, tokenBridgeAddress, wrappedEthAddress); // save the contract address somewhere await fsp.mkdir(scriptsAddressPath, { recursive: true }); diff --git a/contracts/migrations/polygon/2_deploy_contracts.js b/contracts/migrations/polygon/2_deploy_contracts.js index 563b13a..9aa0792 100644 --- a/contracts/migrations/polygon/2_deploy_contracts.js +++ b/contracts/migrations/polygon/2_deploy_contracts.js @@ -7,19 +7,13 @@ const scriptsAddressPath = "../react/src/addresses"; module.exports = async function (deployer, network) { const routerAddress = "0xa5E0829CaCEd8fFDD4De3c43696c57F7D7A678ff"; // quickwap - const feeTokenAddress = "0xe3a1c77e952b57b5883f6c906fc706fcc7d4392c"; // wUST + const feeTokenAddress = "0xcf7BEE494B42cB5A902dF000158037Ad334eB4a7"; // wormUSD const tokenBridgeAddress = "0x377D55a7928c046E18eEbb61977e714d2a76472a"; const wrappedMaticAddress = "0x9c3c9283d3e44854697cd22d3faa240cfb032889"; await deployer.deploy(SwapHelper); await deployer.link(SwapHelper, CrossChainSwapV2); - await deployer.deploy( - CrossChainSwapV2, - routerAddress, - feeTokenAddress, - tokenBridgeAddress, - wrappedMaticAddress - ); + await deployer.deploy(CrossChainSwapV2, routerAddress, feeTokenAddress, tokenBridgeAddress, wrappedMaticAddress); // save the contract address somewhere await fsp.mkdir(scriptsAddressPath, { recursive: true }); diff --git a/contracts/migrations/tokens/1_initial_migration.js b/contracts/migrations/tokens/1_initial_migration.js new file mode 100644 index 0000000..16a7ba5 --- /dev/null +++ b/contracts/migrations/tokens/1_initial_migration.js @@ -0,0 +1,5 @@ +const Migrations = artifacts.require("Migrations"); + +module.exports = function (deployer) { + deployer.deploy(Migrations); +}; diff --git a/contracts/migrations/tokens/2_deploy_contracts.js b/contracts/migrations/tokens/2_deploy_contracts.js new file mode 100644 index 0000000..bce3241 --- /dev/null +++ b/contracts/migrations/tokens/2_deploy_contracts.js @@ -0,0 +1,11 @@ +require("dotenv").config({ path: ".env" }); + +const WormUSD = artifacts.require("WormUSD"); + +module.exports = async function (deployer, network) { + const mintAddress = process.env.mintToAddress; + const tokenDecimals = process.env.decimals; + const tokenSupply = process.env.supply; + + await deployer.deploy(WormUSD, mintAddress, tokenDecimals, tokenSupply); +}; diff --git a/swap_relayer/src/evm.ts b/swap_relayer/src/evm.ts index fe495fe..a3a0593 100644 --- a/swap_relayer/src/evm.ts +++ b/swap_relayer/src/evm.ts @@ -1,7 +1,4 @@ -import { - getIsTransferCompletedEth, - hexToUint8Array, -} from "@certusone/wormhole-sdk"; +import { getIsTransferCompletedEth, hexToUint8Array } from "@certusone/wormhole-sdk"; import { ethers } from "ethers"; @@ -54,35 +51,24 @@ export function loadEvmConfig(): EvmEnvironment[] { } let key_contract_address: string = evm + "_CONTRACT_ADDRESS"; - let val_contract_address: string = eval( - "process.env." + key_contract_address - ); + let val_contract_address: string = eval("process.env." + key_contract_address); if (!val_contract_address) { logger.error("Missing environment variable " + key_contract_address); return undefined; } let key_token_bridge_address: string = evm + "_TOKEN_BRIDGE_ADDRESS"; - let val_token_bridge_address: string = eval( - "process.env." + key_token_bridge_address - ); + let val_token_bridge_address: string = eval("process.env." + key_token_bridge_address); if (!val_token_bridge_address) { logger.error("Missing environment variable " + key_token_bridge_address); return undefined; } let key_wallet_private_key: string = evm + "_WALLET_PRIVATE_KEY"; - let val_wallet_private_key: string = eval( - "process.env." + key_wallet_private_key - ); - if (!val_wallet_private_key) - val_wallet_private_key = process.env.WALLET_PRIVATE_KEY; + let val_wallet_private_key: string = eval("process.env." + key_wallet_private_key); + if (!val_wallet_private_key) val_wallet_private_key = process.env.WALLET_PRIVATE_KEY; if (!val_wallet_private_key) { - logger.error( - "Missing environment variable " + - key_wallet_private_key + - " or WALLET_PRIVATE_KEY" - ); + logger.error("Missing environment variable " + key_wallet_private_key + " or WALLET_PRIVATE_KEY"); return undefined; } @@ -170,72 +156,15 @@ function makeContractDataForEvm(env: EvmEnvironment): EvmContractData { }; } -export function isEvmContract( - contractAddress: string, - chain_id: number -): boolean { +export function isEvmContract(contractAddress: string, chain_id: number): boolean { let ecd = evmContractData.get(chain_id); return ecd && ecd.contractAddress === contractAddress; } -/* - // GOERLI_PROVIDER = Ethereum - // MUMBAI_PROVIDER = Polygon - - if (t3Payload.contractAddress === CROSSCHAINSWAP_CONTRACT_ADDRESS_ETHEREUM) { - // Use one of the V3 swap methods. - } else if (t3Payload.contractAddress === CROSSCHAINSWAP_CONTRACT_ADDRESS_POLYGON) { - // Use one of the V2 swap methods. - } else { - // Error - } - - if (t3Payload.swapFunctionType === 1 && t3Payload.swapCurrencyType === 1) { - // swapExactInFromVaaNative - } else if (t3Payload.swapFunctionType === 1 && t3Payload.swapCurrencyType === 2) { - // swapExactInFromVaaToken - } else if ( - t3Payload.swapFunctionType === 2 && t3Payload.swapCurrencyType === 1) { - // swapExactOutFromVaaNative - } else if (t3Payload.swapFunctionType === 2 && t3Payload.swapCurrencyType === 2) { - // swapExactOutFromVaaToken - } else { - // error - } -*/ - -/* - // GOERLI_PROVIDER = Ethereum - // MUMBAI_PROVIDER = Polygon - - if (t3Payload.contractAddress === CROSSCHAINSWAP_CONTRACT_ADDRESS_ETHEREUM) { - // Use one of the V3 swap methods. - } else if (t3Payload.contractAddress === CROSSCHAINSWAP_CONTRACT_ADDRESS_POLYGON) { - // Use one of the V2 swap methods. - } else { - // Error - } - - if (t3Payload.swapFunctionType === 1 && t3Payload.swapCurrencyType === 1) { - // swapExactInFromVaaNative - } else if (t3Payload.swapFunctionType === 1 && t3Payload.swapCurrencyType === 2) { - // swapExactInFromVaaToken - } else if ( - t3Payload.swapFunctionType === 2 && t3Payload.swapCurrencyType === 1) { - // swapExactOutFromVaaNative - } else if (t3Payload.swapFunctionType === 2 && t3Payload.swapCurrencyType === 2) { - // swapExactOutFromVaaToken - } else { - // error - } -*/ - export async function relayVaaToEvm(vaaBytes: string, t3Payload: Type3Payload) { let ecd = evmContractData.get(t3Payload.targetChainId); if (!ecd) { - logger.error( - "relayVaaToEvm: chain id " + t3Payload.targetChainId + " does not exist!" - ); + logger.error("relayVaaToEvm: chain id " + t3Payload.targetChainId + " does not exist!"); } let exactIn: boolean = false; @@ -244,51 +173,23 @@ export async function relayVaaToEvm(vaaBytes: string, t3Payload: Type3Payload) { exactIn = true; } else if (t3Payload.swapFunctionType !== 2) { error = true; - logger.error( - "relayVaaTo" + - ecd.name + - ": unsupported swapFunctionType: [" + - t3Payload.swapFunctionType + - "]" - ); + logger.error("relayVaaTo" + ecd.name + ": unsupported swapFunctionType: [" + t3Payload.swapFunctionType + "]"); } - - let native: boolean = false; - if (t3Payload.swapCurrencyType === 1) { - native = true; - } else if (t3Payload.swapCurrencyType !== 2) { - error = true; - logger.error( - "relayVaaTo" + - ecd.name + - ": unsupported swapCurrencyType: [" + - t3Payload.swapCurrencyType + - "]" - ); - } - if (error) return; logger.debug( - "relayVaaTo" + - ecd.name + - ": chain_id: " + - ecd.chain_id + - ", contractAddress: [" + - t3Payload.contractAddress + - "]" + "relayVaaTo" + ecd.name + ": chain_id: " + ecd.chain_id + ", contractAddress: [" + t3Payload.contractAddress + "]" ); const signedVaaArray = hexToUint8Array(vaaBytes); - await relayVaaToEvmChain(t3Payload, ecd, signedVaaArray, exactIn, native); + await relayVaaToEvmChain(t3Payload, ecd, signedVaaArray, exactIn); } async function relayVaaToEvmChain( t3Payload: Type3Payload, tcd: EvmContractData, signedVaaArray: Uint8Array, - exactIn: boolean, - native: boolean + exactIn: boolean ) { logger.debug( "relayVaaTo" + @@ -312,8 +213,6 @@ async function relayVaaToEvmChain( t3Payload.contractAddress + "], exactIn: " + exactIn + - ", native: " + - native + ": completed: already transferred" ); @@ -331,41 +230,17 @@ async function relayVaaToEvmChain( t3Payload.contractAddress + "], exactIn: " + exactIn + - ", native: " + - native + ": submitting redeem request" ); try { let receipt: any = null; if (exactIn) { - if (native) { - logger.debug("relayVaaTo: calling evmSwapExactInFromVaaNative()"); - receipt = await swap.evmSwapExactInFromVaaNative( - tcd.contractWithSigner, - signedVaaArray - ); - } else { - logger.debug("relayVaaTo: calling evmSwapExactInFromVaaToken()"); - receipt = await swap.evmSwapExactInFromVaaToken( - tcd.contractWithSigner, - signedVaaArray - ); - } + logger.debug("relayVaaTo: calling evmSwapExactInFromVaaNative()"); + receipt = await swap.evmSwapExactInFromVaaNative(tcd.contractWithSigner, signedVaaArray); } else { - if (native) { - logger.debug("relayVaaTo: calling evmSwapExactOutFromVaaNative()"); - receipt = await swap.evmSwapExactOutFromVaaNative( - tcd.contractWithSigner, - signedVaaArray - ); - } else { - logger.debug("relayVaaTo: calling evmSwapExactOutFromVaaToken()"); - receipt = await swap.evmSwapExactOutFromVaaToken( - tcd.contractWithSigner, - signedVaaArray - ); - } + logger.debug("relayVaaTo: calling evmSwapExactOutFromVaaNative()"); + receipt = await swap.evmSwapExactOutFromVaaNative(tcd.contractWithSigner, signedVaaArray); } logger.info( @@ -379,8 +254,6 @@ async function relayVaaToEvmChain( t3Payload.contractAddress + "], exactIn: " + exactIn + - ", native: " + - native + ": completed: success, txHash: " + receipt.transactionHash ); @@ -397,8 +270,6 @@ async function relayVaaToEvmChain( t3Payload.contractAddress + "], exactIn: " + exactIn + - ", native: " + - native + ": completed: relay failed because the vaa has already been redeemed" ); @@ -412,8 +283,6 @@ async function relayVaaToEvmChain( t3Payload.contractAddress + "], exactIn: " + exactIn + - ", native: " + - native + ": transaction failed: %o", e ); @@ -431,8 +300,6 @@ async function relayVaaToEvmChain( t3Payload.contractAddress + "], exactIn: " + exactIn + - ", native: " + - native + ": redeem confirmed" ); } else { @@ -447,29 +314,18 @@ async function relayVaaToEvmChain( t3Payload.contractAddress + "], exactIn: " + exactIn + - ", native: " + - native + ": completed: failed to confirm redeem!" ); } } -async function isRedeemedOnEvm( - tcd: EvmContractData, - signedVaaArray: Uint8Array -): Promise { +async function isRedeemedOnEvm(tcd: EvmContractData, signedVaaArray: Uint8Array): Promise { let redeemed: boolean = false; try { - redeemed = await getIsTransferCompletedEth( - tcd.tokenBridgeAddress, - tcd.provider, - signedVaaArray - ); + redeemed = await getIsTransferCompletedEth(tcd.tokenBridgeAddress, tcd.provider, signedVaaArray); } catch (e) { logger.error( - "relayVaaTo" + - tcd.name + - ": failed to check if transfer is already complete, will attempt the transfer, e: %o", + "relayVaaTo" + tcd.name + ": failed to check if transfer is already complete, will attempt the transfer, e: %o", e ); } diff --git a/swap_relayer/src/index.ts b/swap_relayer/src/index.ts index 7d33294..b75ca98 100644 --- a/swap_relayer/src/index.ts +++ b/swap_relayer/src/index.ts @@ -12,33 +12,11 @@ import { getEmitterAddressTerra, } from "@certusone/wormhole-sdk"; -import { - importCoreWasm, - setDefaultWasm, -} from "@certusone/wormhole-sdk/lib/cjs/solana/wasm"; - -import { - createSpyRPCServiceClient, - subscribeSignedVAA, -} from "@certusone/wormhole-spydk"; - +import { importCoreWasm, setDefaultWasm } from "@certusone/wormhole-sdk/lib/cjs/solana/wasm"; +import { createSpyRPCServiceClient, subscribeSignedVAA } from "@certusone/wormhole-spydk"; import { ethers } from "ethers"; - -import { - EvmEnvironment, - isEvmContract, - loadEvmConfig, - makeEvmContractData, - relayVaaToEvm, -} from "./evm"; - -import { - isTerraContract, - loadTerraConfig, - makeTerraContractData, - relayVaaToTerra, - TerraEnvironment, -} from "./terra"; +import { EvmEnvironment, isEvmContract, loadEvmConfig, makeEvmContractData, relayVaaToEvm } from "./evm"; +import { isTerraContract, loadTerraConfig, makeTerraContractData, relayVaaToTerra, TerraEnvironment } from "./terra"; export let logger: any; @@ -66,7 +44,6 @@ export type Type3Payload = { contractAddress: string; relayerFee: ethers.BigNumber; swapFunctionType: number; - swapCurrencyType: number; }; type PendingEvent = { @@ -88,11 +65,7 @@ let condition = new CondVar(); let pendingQueue = new Array(); if (success) { - logger.info( - "swap_relayer starting up, will listen for signed VAAs from [" + - env.spy_host + - "]" - ); + logger.info("swap_relayer starting up, will listen for signed VAAs from [" + env.spy_host + "]"); try { makeEvmContractData(env.evm_configs); @@ -143,10 +116,7 @@ async function spy_listen() { var myFilters = []; for (var i = 0; i < parsedJsonFilters.length; i++) { var myChainId = parseInt(parsedJsonFilters[i].chain_id) as ChainId; - var myEmitterAddress = await encodeEmitterAddress( - myChainId, - parsedJsonFilters[i].emitter_address - ); + var myEmitterAddress = await encodeEmitterAddress(myChainId, parsedJsonFilters[i].emitter_address); var myEmitterFilter = { emitterFilter: { chainId: myChainId, @@ -182,10 +152,7 @@ async function spy_listen() { })(); } -async function encodeEmitterAddress( - myChainId, - emitterAddressStr -): Promise { +async function encodeEmitterAddress(myChainId, emitterAddressStr): Promise { if (myChainId === CHAIN_ID_SOLANA) { return await getEmitterAddressSolana(emitterAddressStr); } @@ -206,17 +173,11 @@ async function processVaa(vaaBytes: string) { let emitter_address: string = uint8ArrayToHex(parsedVAA.emitter_address); - let seqNumKey: string = - parsedVAA.emitter_chain.toString() + ":" + emitter_address; + let seqNumKey: string = parsedVAA.emitter_chain.toString() + ":" + emitter_address; let lastSeqNum = seqMap.get(seqNumKey); if (lastSeqNum) { if (lastSeqNum >= parsedVAA.sequence) { - logger.debug( - "ignoring duplicate: emitter: [" + - seqNumKey + - "], seqNum: " + - parsedVAA.sequence - ); + logger.debug("ignoring duplicate: emitter: [" + seqNumKey + "], seqNum: " + parsedVAA.sequence); return; } } @@ -248,8 +209,6 @@ async function processVaa(vaaBytes: string) { t3Payload.relayerFee + "], swapFunctionType: [" + t3Payload.swapFunctionType + - "], swapCurrencyType: [" + - t3Payload.swapCurrencyType + "]" ); @@ -270,8 +229,6 @@ async function processVaa(vaaBytes: string) { t3Payload.relayerFee + "], swapFunctionType: [" + t3Payload.swapFunctionType + - "], swapCurrencyType: [" + - t3Payload.swapCurrencyType + "]" ); } @@ -289,10 +246,7 @@ async function processVaa(vaaBytes: string) { } } -function decodeSignedVAAPayloadType3( - parsedVAA: any, - sourceChainId: number -): Type3Payload { +function decodeSignedVAAPayloadType3(parsedVAA: any, sourceChainId: number): Type3Payload { const payload = Buffer.from(new Uint8Array(parsedVAA.payload)); if (payload[0] !== 3) return undefined; @@ -310,18 +264,13 @@ function decodeSignedVAAPayloadType3( let contractAddress: string = ""; let swapFunctionType: number = 0; - let swapCurrencyType: number = 0; if (targetChainId === 3) { - logger.info( - "decodeSignedVAAPayloadType3: terraContractAddr: [" + - payload.slice(67, 67 + 32).toString("hex") + - "]" - ); + logger.info("decodeSignedVAAPayloadType3: terraContractAddr: [" + payload.slice(67, 67 + 32).toString("hex") + "]"); contractAddress = payload.slice(67, 67 + 32).toString("hex"); } else { - if (payload.length < 262) { + if (payload.length < 272) { logger.error( "decodeSignedVAAPayloadType3: dropping type 3 vaa because the payload is too short to extract the contract fields, length: " + payload.length + @@ -330,34 +279,24 @@ function decodeSignedVAAPayloadType3( ); return undefined; } - contractAddress = payload.slice(79, 79 + 20).toString("hex"); swapFunctionType = payload.readUInt8(272); - swapCurrencyType = payload.readUInt8(273); } return { sourceChainId: sourceChainId, targetChainId: targetChainId, contractAddress: contractAddress, - relayerFee: ethers.BigNumber.from(payload.slice(101, 101 + 32)), + relayerFee: ethers.BigNumber.from(payload.slice(273, 273 + 32)), swapFunctionType: swapFunctionType, - swapCurrencyType: swapCurrencyType, }; } function isOurContract(contractAddress: string, chainId: number): boolean { - return ( - isEvmContract(contractAddress, chainId) || - isTerraContract(contractAddress, chainId) - ); + return isEvmContract(contractAddress, chainId) || isTerraContract(contractAddress, chainId); } -async function postVaa( - vaaBytes: any, - t3Payload: Type3Payload, - receiveTime: Date -) { +async function postVaa(vaaBytes: any, t3Payload: Type3Payload, receiveTime: Date) { let event: PendingEvent = { vaaBytes: vaaBytes, t3Payload: t3Payload, @@ -366,9 +305,7 @@ async function postVaa( await mutex.runExclusive(() => { pendingQueue.push(event); - logger.debug( - "posting event, there are now " + pendingQueue.length + " enqueued events" - ); + logger.debug("posting event, there are now " + pendingQueue.length + " enqueued events"); if (condition) { logger.debug("hitting condition variable."); condition.complete(true); @@ -419,16 +356,12 @@ async function callBack(err: any, result: any) { await mutex.runExclusive(async () => { if (pendingQueue.length === 0) { - logger.debug( - "in callback, no more pending events, rearming the condition." - ); + logger.debug("in callback, no more pending events, rearming the condition."); done = true; condition = new CondVar(); await condition.wait(COND_VAR_TIMEOUT, callBack); } else { - logger.debug( - "in callback, there are " + pendingQueue.length + " pending events." - ); + logger.debug("in callback, there are " + pendingQueue.length + " pending events."); } }); } @@ -455,8 +388,7 @@ function initLogger() { let logFileName: string = ""; if (process.env.LOG_DIR) { useConsole = false; - logFileName = - process.env.LOG_DIR + "/swap_relay." + new Date().toISOString() + ".log"; + logFileName = process.env.LOG_DIR + "/swap_relay." + new Date().toISOString() + ".log"; } let logLevel = "info"; @@ -472,11 +404,7 @@ function initLogger() { level: logLevel, }); } else { - console.log( - "swap_relay is logging to [%s] at level [%s]", - logFileName, - logLevel - ); + console.log("swap_relay is logging to [%s] at level [%s]", logFileName, logLevel); transport = new winston.transports.File({ filename: logFileName, @@ -492,9 +420,7 @@ function initLogger() { winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss.SSS", }), - winston.format.printf( - (info: any) => `${[info.timestamp]}|${info.level}|${info.message}` - ) + winston.format.printf((info: any) => `${[info.timestamp]}|${info.level}|${info.message}`) ), };