diff --git a/evm/.gitignore b/evm/.gitignore index db41883..5902272 100644 --- a/evm/.gitignore +++ b/evm/.gitignore @@ -3,3 +3,7 @@ /lib/ /node_modules/ addresses.json +anvil.log +deploy.out +cache +broadcast diff --git a/evm/contracts/FastTransfer.sol b/evm/contracts/FastTransfer.sol index 36d7ea8..179f966 100644 --- a/evm/contracts/FastTransfer.sol +++ b/evm/contracts/FastTransfer.sol @@ -6,70 +6,34 @@ pragma solidity >=0.8.0 <0.9.0; import "./libraries/external/BytesLib.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -interface IWormhole { - function publishMessage( - uint32 nonce, - bytes memory payload, - uint8 consistencyLevel - ) external payable returns (uint64 sequence); - function messageFee() external view returns (uint256); -} -interface ITokenBridge { - function wrapAndTransferETH( - uint16 recipientChain, - bytes32 recipient, - uint256 arbiterFee, - uint32 nonce - ) external payable returns (uint64 sequence); - function chainId() external view returns (uint16); - function WETH() external view returns (IWETH); -} -interface IWETH is IERC20 { - function deposit() external payable; - function withdraw(uint amount) external; -} +import "./interfaces/ITokenBridge.sol"; -// reuse Portal Transfer struct for convenience -struct Transfer { - // PayloadID uint8 = 1 - uint8 payloadID; - // Amount being transferred (big-endian uint256) - uint256 amount; - // Address of the token. Left-zero-padded if shorter than 32 bytes - bytes32 tokenAddress; - // Chain ID of the token - uint16 tokenChain; - // Address of the recipient. Left-zero-padded if shorter than 32 bytes - bytes32 to; - // Chain ID of the recipient - uint16 toChain; - // Amount of tokens (big-endian uint256) that the user is willing to pay as relayer fee. Must be <= Amount. - uint256 fee; -} - -contract FastTransfer { - - IWormhole wormhole; - ITokenBridge portal; - - constructor(address wormholeAddress, address portalAddress) { - wormhole = IWormhole(wormholeAddress); - portal = ITokenBridge(portalAddress); - } +import "./FastTransferMessages.sol"; +import "./FastTransferSetters.sol"; +contract FastTransfer is FastTransferMessages, FastTransferSetters { function wrapAndTransferETH( uint16 recipientChain, bytes32 recipient, uint256 arbiterFee, uint32 nonce ) public payable returns (uint64 fastSequence, uint64 portalSequence) { + // cache Wormhole and Portal instance + IWormhole wormhole = wormhole(); + ITokenBridge portal = portal(); + // Portal accounts for 1 fee, but we must account for 2 uint wormholeFee = wormhole.messageFee(); require(wormholeFee * 2 < msg.value, "value is smaller than wormhole fees"); + + // compute amound less fees uint amount = msg.value - wormholeFee * 2; - // Portal will normalize the amount to 8 decimals, so we should do the same + + // normalize amount the same way that Portal does uint normalizedAmount = normalizeAmount(amount, 18); - Transfer memory fastTransfer = Transfer({ + + // create fast transfer message and publish it + ITokenBridge.Transfer memory fastTransfer = ITokenBridge.Transfer({ payloadID: 1, amount: normalizedAmount, tokenAddress: bytes32(uint256(uint160(address(portal.WETH())))), @@ -78,21 +42,15 @@ contract FastTransfer { toChain: recipientChain, fee: 0 }); + fastSequence = wormhole.publishMessage{ value : wormholeFee }( - 0, - abi.encodePacked( - fastTransfer.payloadID, - fastTransfer.amount, - fastTransfer.tokenAddress, - fastTransfer.tokenChain, - fastTransfer.to, - fastTransfer.toChain, - fastTransfer.fee - ), - 200 + nonce, + encodeFastTransfer(fastTransfer), + finality(true) ); + // Forward the remaining value sans the first fee portalSequence = portal.wrapAndTransferETH{ value: msg.value - wormholeFee diff --git a/evm/contracts/FastTransferGetters.sol b/evm/contracts/FastTransferGetters.sol new file mode 100644 index 0000000..bf6de6c --- /dev/null +++ b/evm/contracts/FastTransferGetters.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache 2 + +pragma solidity >=0.8.0 <0.9.0; + +import "./libraries/external/BytesLib.sol"; + +import "./interfaces/ITokenBridge.sol"; + +import "./FastTransferState.sol"; + +contract FastTransferGetters is FastTransferState { + function owner() public view returns (address) { + return _state.owner; + } + + function isInitialized(address impl) public view returns (bool) { + return _state.initializedImplementations[impl]; + } + + function chainId() public view returns (uint16) { + return _state.chainId; + } + + function wormhole() internal view returns (IWormhole) { + return _state.wormhole; + } + + function portal() public view returns (ITokenBridge) { + return _state.portal; + } + + function finality(bool isFast) public view returns (uint8) { + return isFast ? _state.fastFinality : _state.finality; + } +} diff --git a/evm/contracts/FastTransferImplementation.sol b/evm/contracts/FastTransferImplementation.sol new file mode 100644 index 0000000..8df91e9 --- /dev/null +++ b/evm/contracts/FastTransferImplementation.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Apache 2 + +pragma solidity >=0.8.0 <0.9.0; + +import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Upgrade.sol"; + +import "./FastTransfer.sol"; + +contract FastTransferImplementation is FastTransfer, ERC1967Upgrade { + function initialize() initializer public virtual { + // this function needs to be exposed for an upgrade to pass + } + + modifier initializer() { + address impl = ERC1967Upgrade._getImplementation(); + + require( + !isInitialized(impl), + "already initialized" + ); + + setInitialized(impl); + + _; + } +} \ No newline at end of file diff --git a/evm/contracts/FastTransferMessages.sol b/evm/contracts/FastTransferMessages.sol new file mode 100644 index 0000000..aae0d33 --- /dev/null +++ b/evm/contracts/FastTransferMessages.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Apache 2 + +pragma solidity >=0.8.0 <0.9.0; + +import "./libraries/external/BytesLib.sol"; + +import "./FastTransferGetters.sol"; + +contract FastTransferMessages is FastTransferGetters { + function encodeFastTransfer(ITokenBridge.Transfer memory transfer) public view returns (bytes memory) { + return portal().encodeTransfer(transfer); + } +} diff --git a/evm/contracts/FastTransferProxy.sol b/evm/contracts/FastTransferProxy.sol new file mode 100644 index 0000000..f0923a6 --- /dev/null +++ b/evm/contracts/FastTransferProxy.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache 2 + +pragma solidity >=0.8.0 <0.9.0; + +import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +contract FastTransferProxy is ERC1967Proxy { + constructor (address implementation, bytes memory initData) + ERC1967Proxy( + implementation, + initData + ) + {} +} \ No newline at end of file diff --git a/evm/contracts/FastTransferSetters.sol b/evm/contracts/FastTransferSetters.sol new file mode 100644 index 0000000..747507a --- /dev/null +++ b/evm/contracts/FastTransferSetters.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache 2 + +pragma solidity >=0.8.0 <0.9.0; + +import "./interfaces/ITokenBridge.sol"; + +import "./FastTransferState.sol"; + +contract FastTransferSetters is FastTransferState { + function setOwner(address owner) internal { + _state.owner = owner; + } + + function setInitialized(address implementatiom) internal { + _state.initializedImplementations[implementatiom] = true; + } + + function setChainId(uint16 chainId) internal { + _state.chainId = chainId; + } + + function setWormhole(address wormholeAddress) internal { + _state.wormhole = IWormhole(payable(wormholeAddress)); + } + + function setPortal(address portalAddress) internal { + _state.portal = ITokenBridge(payable(portalAddress)); + } + + function setFinality(uint8 finality_) internal { + _state.finality = finality_; + } + + function setFastFinality(uint8 fastFinality_) internal { + _state.fastFinality = fastFinality_; + } +} diff --git a/evm/contracts/FastTransferSetup.sol b/evm/contracts/FastTransferSetup.sol new file mode 100644 index 0000000..3a89f7b --- /dev/null +++ b/evm/contracts/FastTransferSetup.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: Apache 2 + +pragma solidity >=0.8.0 <0.9.0; + +import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Upgrade.sol"; +import {Context} from "@openzeppelin/contracts/utils/Context.sol"; + +import "./FastTransferSetters.sol"; + +contract FastTransferSetup is FastTransferSetters, ERC1967Upgrade, Context { + function setup( + address implementation, + uint16 chainId, + address wormhole, + address portal, + uint8 finality + ) public { + require(wormhole != address(0), "invalid wormhole address"); + require(portal != address(0), "invalid portal address"); + require(implementation != address(0), "invalid implementation"); + + setOwner(_msgSender()); + + setChainId(chainId); + + setWormhole(wormhole); + + setPortal(portal); + + setFinality(finality); + + // fast finality is always 200 + setFastFinality(200); + + _upgradeTo(implementation); + + /// @dev call initialize function of the new implementation + (bool success, bytes memory reason) = implementation.delegatecall(abi.encodeWithSignature("initialize()")); + require(success, string(reason)); + } +} \ No newline at end of file diff --git a/evm/contracts/FastTransferState.sol b/evm/contracts/FastTransferState.sol new file mode 100644 index 0000000..985aeb8 --- /dev/null +++ b/evm/contracts/FastTransferState.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache 2 + +pragma solidity >=0.8.0 <0.9.0; + +import "./interfaces/ITokenBridge.sol"; + +contract FastTransferStorage { + struct State { + // address of contract owner + address owner; + + // chainId of this contract + uint16 chainId; + + // wormhole message finality + uint8 finality; + + // portal finality for fast transfers + uint8 fastFinality; + + // portal instance + ITokenBridge portal; + + // wormhole instance + IWormhole wormhole; + + /// mapping of initialized implementations + mapping(address => bool) initializedImplementations; + + // storage gap + uint256[50] ______gap; + } +} + +contract FastTransferState { + FastTransferStorage.State _state; +} \ No newline at end of file diff --git a/evm/contracts/interfaces/ITokenBridge.sol b/evm/contracts/interfaces/ITokenBridge.sol new file mode 100644 index 0000000..ad0ba50 --- /dev/null +++ b/evm/contracts/interfaces/ITokenBridge.sol @@ -0,0 +1,93 @@ +// contracts/Bridge.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +import "./IWETH.sol"; +import "./IWormhole.sol"; + +interface ITokenBridge { + struct Transfer { + uint8 payloadID; + uint256 amount; + bytes32 tokenAddress; + uint16 tokenChain; + bytes32 to; + uint16 toChain; + uint256 fee; + } + + struct TransferWithPayload { + uint8 payloadID; + uint256 amount; + bytes32 tokenAddress; + uint16 tokenChain; + bytes32 to; + uint16 toChain; + bytes32 fromAddress; + bytes payload; + } + + struct AssetMeta { + uint8 payloadID; + bytes32 tokenAddress; + uint16 tokenChain; + uint8 decimals; + bytes32 symbol; + bytes32 name; + } + + function attestToken(address tokenAddress, uint32 nonce) external payable returns (uint64 sequence); + + function wrapAndTransferETH(uint16 recipientChain, bytes32 recipient, uint256 arbiterFee, uint32 nonce) external payable returns (uint64 sequence); + + function wrapAndTransferETHWithPayload(uint16 recipientChain, bytes32 recipient, uint32 nonce, bytes memory payload) external payable returns (uint64 sequence); + + function transferTokens(address token, uint256 amount, uint16 recipientChain, bytes32 recipient, uint256 arbiterFee, uint32 nonce) external payable returns (uint64 sequence); + + function transferTokensWithPayload(address token, uint256 amount, uint16 recipientChain, bytes32 recipient, uint32 nonce, bytes memory payload) external payable returns (uint64 sequence); + + function updateWrapped(bytes memory encodedVm) external returns (address token); + + function createWrapped(bytes memory encodedVm) external returns (address token); + + function completeTransferWithPayload(bytes memory encodedVm) external returns (bytes memory); + + function completeTransferAndUnwrapETHWithPayload(bytes memory encodedVm) external returns (bytes memory); + + function completeTransfer(bytes memory encodedVm) external; + + function completeTransferAndUnwrapETH(bytes memory encodedVm) external; + + function encodeAssetMeta(AssetMeta memory meta) external pure returns (bytes memory encoded); + + function encodeTransfer(Transfer memory transfer) external pure returns (bytes memory encoded); + + function encodeTransferWithPayload(TransferWithPayload memory transfer) external pure returns (bytes memory encoded); + + function parsePayloadID(bytes memory encoded) external pure returns (uint8 payloadID); + + function parseAssetMeta(bytes memory encoded) external pure returns (AssetMeta memory meta); + + function parseTransfer(bytes memory encoded) external pure returns (Transfer memory transfer); + + function parseTransferWithPayload(bytes memory encoded) external pure returns (TransferWithPayload memory transfer); + + function isTransferCompleted(bytes32 hash) external view returns (bool); + + function wormhole() external view returns (IWormhole); + + function chainId() external view returns (uint16); + + function evmChainId() external view returns (uint256); + + function wrappedAsset(uint16 tokenChainId, bytes32 tokenAddress) external view returns (address); + + function WETH() external view returns (IWETH); + + function outstandingBridged(address token) external view returns (uint256); + + function isWrappedAsset(address token) external view returns (bool); + + function finality() external view returns (uint8); +} \ No newline at end of file diff --git a/evm/contracts/interfaces/IWETH.sol b/evm/contracts/interfaces/IWETH.sol new file mode 100644 index 0000000..dbea141 --- /dev/null +++ b/evm/contracts/interfaces/IWETH.sol @@ -0,0 +1,11 @@ +// contracts/Bridge.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +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/evm/contracts/interfaces/IWormhole.sol b/evm/contracts/interfaces/IWormhole.sol new file mode 100644 index 0000000..1754227 --- /dev/null +++ b/evm/contracts/interfaces/IWormhole.sol @@ -0,0 +1,72 @@ +// contracts/Messages.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +interface IWormhole { + struct GuardianSet { + address[] keys; + uint32 expirationTime; + } + + struct Signature { + bytes32 r; + bytes32 s; + uint8 v; + uint8 guardianIndex; + } + + struct VM { + uint8 version; + uint32 timestamp; + uint32 nonce; + uint16 emitterChainId; + bytes32 emitterAddress; + uint64 sequence; + uint8 consistencyLevel; + bytes payload; + + uint32 guardianSetIndex; + Signature[] signatures; + + bytes32 hash; + } + + event LogMessagePublished(address indexed sender, uint64 sequence, uint32 nonce, bytes payload, uint8 consistencyLevel); + + function publishMessage( + uint32 nonce, + bytes memory payload, + uint8 consistencyLevel + ) external payable returns (uint64 sequence); + + function parseAndVerifyVM(bytes calldata encodedVM) external view returns (VM memory vm, bool valid, string memory reason); + + function verifyVM(VM memory vm) external view returns (bool valid, string memory reason); + + function verifySignatures(bytes32 hash, Signature[] memory signatures, GuardianSet memory guardianSet) external pure returns (bool valid, string memory reason); + + function parseVM(bytes memory encodedVM) external pure returns (VM memory vm); + + function getGuardianSet(uint32 index) external view returns (GuardianSet memory); + + function getCurrentGuardianSetIndex() external view returns (uint32); + + function getGuardianSetExpiry() external view returns (uint32); + + function governanceActionIsConsumed(bytes32 hash) external view returns (bool); + + function isInitialized(address impl) external view returns (bool); + + function chainId() external view returns (uint16); + + function governanceChainId() external view returns (uint16); + + function governanceContract() external view returns (bytes32); + + function messageFee() external view returns (uint256); + + function evmChainId() external view returns (uint256); + + function nextSequence(address emitter) external view returns (uint64); +} diff --git a/evm/forge-test/FastTransfer.t.sol b/evm/forge-test/FastTransfer.t.sol new file mode 100644 index 0000000..be48e06 --- /dev/null +++ b/evm/forge-test/FastTransfer.t.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +import "../contracts/FastTransfer.sol"; + +contract TestFastTransfer is Test { + using BytesLib for bytes; +} \ No newline at end of file diff --git a/evm/foundry.toml b/evm/foundry.toml new file mode 100644 index 0000000..235e399 --- /dev/null +++ b/evm/foundry.toml @@ -0,0 +1,11 @@ +[profile.default] +src = 'contracts' +out = 'build' +test = 'forge-test' + +libs = [ + "lib", + "node_modules" +] + +# See more config options https://github.com/foundry-rs/foundry/tree/master/config