-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Connectors: Implement wormhole bridge connector (#15)
- Loading branch information
Showing
9 changed files
with
284 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
27 changes: 27 additions & 0 deletions
27
packages/connectors/contracts/bridge/wormhole/IWormhole.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
// SPDX-License-Identifier: GPL-3.0-or-later | ||
// This program is free software: you can redistribute it and/or modify | ||
// it under the terms of the GNU General Public License as published by | ||
// the Free Software Foundation, either version 3 of the License, or | ||
// (at your option) any later version. | ||
|
||
// This program is distributed in the hope that it will be useful, | ||
// but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
// GNU General Public License for more details. | ||
|
||
// You should have received a copy of the GNU General Public License | ||
// along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
|
||
pragma solidity >=0.8.0; | ||
|
||
interface IWormhole { | ||
function transferTokensWithRelay( | ||
address token, | ||
uint256 amount, | ||
uint256 toNativeTokenAmount, | ||
uint16 targetChain, | ||
bytes32 targetRecipientWallet | ||
) external payable returns (uint64 messageSequence); | ||
|
||
function relayerFee(uint16 chainId, address token) external view returns (uint256); | ||
} |
106 changes: 106 additions & 0 deletions
106
packages/connectors/contracts/bridge/wormhole/WormholeConnector.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
// SPDX-License-Identifier: GPL-3.0-or-later | ||
// This program is free software: you can redistribute it and/or modify | ||
// it under the terms of the GNU General Public License as published by | ||
// the Free Software Foundation, either version 3 of the License, or | ||
// (at your option) any later version. | ||
|
||
// This program is distributed in the hope that it will be useful, | ||
// but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
// GNU General Public License for more details. | ||
|
||
// You should have received a copy of the GNU General Public License | ||
// along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
|
||
pragma solidity ^0.8.0; | ||
|
||
import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; | ||
import '@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol'; | ||
|
||
import '@mimic-fi/v3-helpers/contracts/utils/ERC20Helpers.sol'; | ||
|
||
import './IWormhole.sol'; | ||
|
||
/** | ||
* @title WormholeConnector | ||
* @dev Interfaces with Wormhole to bridge tokens through CCTP | ||
*/ | ||
contract WormholeConnector { | ||
// List of Wormhole network IDs | ||
uint16 private constant ETHEREUM_WORMHOLE_NETWORK_ID = 2; | ||
uint16 private constant POLYGON_WORMHOLE_NETWORK_ID = 5; | ||
uint16 private constant ARBITRUM_WORMHOLE_NETWORK_ID = 23; | ||
uint16 private constant OPTIMISM_WORMHOLE_NETWORK_ID = 24; | ||
uint16 private constant BSC_WORMHOLE_NETWORK_ID = 4; | ||
uint16 private constant FANTOM_WORMHOLE_NETWORK_ID = 10; | ||
uint16 private constant AVALANCHE_WORMHOLE_NETWORK_ID = 6; | ||
|
||
// List of chain IDs supported by Wormhole | ||
uint256 private constant ETHEREUM_ID = 1; | ||
uint256 private constant POLYGON_ID = 137; | ||
uint256 private constant ARBITRUM_ID = 42161; | ||
uint256 private constant OPTIMISM_ID = 10; | ||
uint256 private constant BSC_ID = 56; | ||
uint256 private constant FANTOM_ID = 250; | ||
uint256 private constant AVALANCHE_ID = 43114; | ||
|
||
// Reference to the Wormhole's CircleRelayer contract of the source chain | ||
IWormhole public immutable wormholeCircleRelayer; | ||
|
||
/** | ||
* @dev Creates a new Wormhole connector | ||
* @param _wormholeCircleRelayer Address of the Wormhole's CircleRelayer contract for the source chain | ||
*/ | ||
constructor(address _wormholeCircleRelayer) { | ||
wormholeCircleRelayer = IWormhole(_wormholeCircleRelayer); | ||
} | ||
|
||
/** | ||
* @dev Executes a bridge of assets using Wormhole's CircleRelayer integration | ||
* @param chainId ID of the destination chain | ||
* @param token Address of the token to be bridged | ||
* @param amountIn Amount of tokens to be bridged | ||
* @param minAmountOut Minimum amount of tokens willing to receive on the destination chain | ||
* @param recipient Address that will receive the tokens on the destination chain | ||
*/ | ||
function execute(uint256 chainId, address token, uint256 amountIn, uint256 minAmountOut, address recipient) | ||
external | ||
{ | ||
require(block.chainid != chainId, 'WORMHOLE_BRIDGE_SAME_CHAIN'); | ||
require(recipient != address(0), 'WORMHOLE_BRIDGE_RECIPIENT_ZERO'); | ||
|
||
uint16 wormholeNetworkId = _getWormholeNetworkId(chainId); | ||
uint256 relayerFee = wormholeCircleRelayer.relayerFee(wormholeNetworkId, token); | ||
require(minAmountOut <= amountIn - relayerFee, 'WORMHOLE_MIN_AMOUNT_OUT_TOO_BIG'); | ||
|
||
uint256 preBalanceIn = IERC20(token).balanceOf(address(this)); | ||
|
||
ERC20Helpers.approve(token, address(wormholeCircleRelayer), amountIn); | ||
wormholeCircleRelayer.transferTokensWithRelay( | ||
token, | ||
amountIn, | ||
0, // don't swap to native token | ||
wormholeNetworkId, | ||
bytes32(uint256(uint160(recipient))) // convert from address to bytes32 | ||
); | ||
|
||
uint256 postBalanceIn = IERC20(token).balanceOf(address(this)); | ||
require(postBalanceIn >= preBalanceIn - amountIn, 'WORMHOLE_BAD_TOKEN_IN_BALANCE'); | ||
} | ||
|
||
/** | ||
* @dev Tells the Wormhole network ID based on a chain ID | ||
* @param chainId ID of the chain being queried | ||
* @return Wormhole network ID associated to the requested chain ID | ||
*/ | ||
function _getWormholeNetworkId(uint256 chainId) internal pure returns (uint16) { | ||
if (chainId == ETHEREUM_ID) return ETHEREUM_WORMHOLE_NETWORK_ID; | ||
else if (chainId == POLYGON_ID) return POLYGON_WORMHOLE_NETWORK_ID; | ||
else if (chainId == ARBITRUM_ID) return ARBITRUM_WORMHOLE_NETWORK_ID; | ||
else if (chainId == OPTIMISM_ID) return OPTIMISM_WORMHOLE_NETWORK_ID; | ||
else if (chainId == BSC_ID) return BSC_WORMHOLE_NETWORK_ID; | ||
else if (chainId == FANTOM_ID) return FANTOM_WORMHOLE_NETWORK_ID; | ||
else if (chainId == AVALANCHE_ID) return AVALANCHE_WORMHOLE_NETWORK_ID; | ||
else revert('WORMHOLE_UNKNOWN_CHAIN_ID'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
22 changes: 22 additions & 0 deletions
22
packages/connectors/test/bridge/wormhole/WormholeConnector.avalanche.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { deploy } from '@mimic-fi/v3-helpers' | ||
|
||
import { itBehavesLikeWormholeConnector } from './WormholeConnector.behavior' | ||
|
||
/* eslint-disable no-secrets/no-secrets */ | ||
|
||
const USDC = '0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E' | ||
const WHALE = '0xbbff2a8ec8d702e61faaccf7cf705968bb6a5bab' | ||
|
||
const WORMHOLE_CIRCLE_RELAYER = '0x32DeC3F4A0723Ce02232f87e8772024E0C86d834' | ||
|
||
describe('WormholeConnector', () => { | ||
const SOURCE_CHAIN_ID = 43114 | ||
|
||
before('create bridge connector', async function () { | ||
this.connector = await deploy('WormholeConnector', [WORMHOLE_CIRCLE_RELAYER]) | ||
}) | ||
|
||
context('USDC', () => { | ||
itBehavesLikeWormholeConnector(SOURCE_CHAIN_ID, USDC, WHALE) | ||
}) | ||
}) |
93 changes: 93 additions & 0 deletions
93
packages/connectors/test/bridge/wormhole/WormholeConnector.behavior.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import { bn, fp, impersonate, instanceAt, ZERO_ADDRESS } from '@mimic-fi/v3-helpers' | ||
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address' | ||
import { expect } from 'chai' | ||
import { BigNumber, Contract } from 'ethers' | ||
|
||
export function itBehavesLikeWormholeConnector( | ||
sourceChainId: number, | ||
tokenAddress: string, | ||
whaleAddress: string | ||
): void { | ||
let token: Contract, whale: SignerWithAddress | ||
|
||
before('load tokens and accounts', async function () { | ||
token = await instanceAt('IERC20Metadata', tokenAddress) | ||
whale = await impersonate(whaleAddress, fp(100)) | ||
}) | ||
|
||
context('when the recipient is not the zero address', async () => { | ||
let amountIn: BigNumber | ||
let minAmountOut: BigNumber | ||
|
||
const relayerFee = bn(35000000) | ||
|
||
beforeEach('set amount in and min amount out', async () => { | ||
const decimals = await token.decimals() | ||
amountIn = bn(300).mul(bn(10).pow(decimals)) | ||
minAmountOut = amountIn.sub(relayerFee) | ||
}) | ||
|
||
function bridgesProperly(destinationChainId: number) { | ||
if (destinationChainId != sourceChainId) { | ||
it('should send the tokens to the gateway', async function () { | ||
const previousSenderBalance = await token.balanceOf(whale.address) | ||
const previousTotalSupply = await token.totalSupply() | ||
const previousConnectorBalance = await token.balanceOf(this.connector.address) | ||
|
||
await token.connect(whale).transfer(this.connector.address, amountIn) | ||
await this.connector | ||
.connect(whale) | ||
.execute(destinationChainId, tokenAddress, amountIn, minAmountOut, whale.address) | ||
|
||
const currentSenderBalance = await token.balanceOf(whale.address) | ||
expect(currentSenderBalance).to.be.equal(previousSenderBalance.sub(amountIn)) | ||
|
||
// check tokens are burnt on the source chain | ||
const currentTotalSupply = await token.totalSupply() | ||
expect(currentTotalSupply).to.be.equal(previousTotalSupply.sub(amountIn)) | ||
|
||
const currentConnectorBalance = await token.balanceOf(this.connector.address) | ||
expect(currentConnectorBalance).to.be.equal(previousConnectorBalance) | ||
}) | ||
} else { | ||
it('reverts', async function () { | ||
await expect( | ||
this.connector | ||
.connect(whale) | ||
.execute(destinationChainId, tokenAddress, amountIn, minAmountOut, whale.address) | ||
).to.be.revertedWith('WORMHOLE_BRIDGE_SAME_CHAIN') | ||
}) | ||
} | ||
} | ||
|
||
context('bridge to avalanche', () => { | ||
const destinationChainId = 43114 | ||
|
||
bridgesProperly(destinationChainId) | ||
}) | ||
|
||
context('bridge to mainnet', () => { | ||
const destinationChainId = 1 | ||
|
||
bridgesProperly(destinationChainId) | ||
}) | ||
|
||
context('bridge to goerli', () => { | ||
const destinationChainId = 5 | ||
|
||
it('reverts', async function () { | ||
await expect( | ||
this.connector.connect(whale).execute(destinationChainId, tokenAddress, amountIn, minAmountOut, whale.address) | ||
).to.be.revertedWith('WORMHOLE_UNKNOWN_CHAIN_ID') | ||
}) | ||
}) | ||
}) | ||
|
||
context('when the recipient is the zero address', async () => { | ||
it('reverts', async function () { | ||
await expect(this.connector.connect(whale).execute(0, tokenAddress, 0, 0, ZERO_ADDRESS)).to.be.revertedWith( | ||
'WORMHOLE_BRIDGE_RECIPIENT_ZERO' | ||
) | ||
}) | ||
}) | ||
} |
22 changes: 22 additions & 0 deletions
22
packages/connectors/test/bridge/wormhole/WormholeConnector.mainnet.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { deploy } from '@mimic-fi/v3-helpers' | ||
|
||
import { itBehavesLikeWormholeConnector } from './WormholeConnector.behavior' | ||
|
||
/* eslint-disable no-secrets/no-secrets */ | ||
|
||
const USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' | ||
const WHALE = '0xf584f8728b874a6a5c7a8d4d387c9aae9172d621' | ||
|
||
const WORMHOLE_CIRCLE_RELAYER = '0x32DeC3F4A0723Ce02232f87e8772024E0C86d834' | ||
|
||
describe('WormholeConnector', () => { | ||
const SOURCE_CHAIN_ID = 1 | ||
|
||
before('create wormhole connector', async function () { | ||
this.connector = await deploy('WormholeConnector', [WORMHOLE_CIRCLE_RELAYER]) | ||
}) | ||
|
||
context('USDC', () => { | ||
itBehavesLikeWormholeConnector(SOURCE_CHAIN_ID, USDC, WHALE) | ||
}) | ||
}) |