Set up CrossChainUSDC contracts and implemenet transferTokens method

This commit is contained in:
gator-boi 2022-10-14 18:56:46 +00:00
parent 45c11abf85
commit c697a27255
No known key found for this signature in database
GPG Key ID: 09C00ED7DAE976E7
15 changed files with 505 additions and 245 deletions

View File

@ -1,113 +0,0 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {IWormhole} from "../interfaces/IWormhole.sol";
import "../libraries/BytesLib.sol";
import "./HelloWorldGetters.sol";
import "./HelloWorldMessages.sol";
contract HelloWorld is HelloWorldGetters, HelloWorldMessages {
using BytesLib for bytes;
constructor(address wormhole_, uint16 chainId_, uint8 wormholeFinality_) {
// sanity check input values
require(wormhole_ != address(0), "invalid wormhole address");
require(chainId_ > 0, "invalid chainId");
require(wormholeFinality_ > 0, "invalid wormholeFinality");
// set constructor state values
setOwner(msg.sender);
setWormhole(wormhole_);
setChainId(chainId_);
setWormholeFinality(wormholeFinality_);
}
function sendMessage() public payable returns (uint64 messageSequence) {
// cache wormhole instance and fees to save on gas
IWormhole wormhole = wormhole();
uint256 wormholeFee = wormhole.messageFee();
// Confirm that the caller has sent enough ether to pay for the wormhole
// message fee.
require(msg.value == wormholeFee, "insufficient value");
// create the HelloWorldMessage struct
HelloWorldMessage memory parsedMessage = HelloWorldMessage({
payloadID: uint8(1),
message: "HelloSolana"
});
// encode the message
bytes memory encodedMessage = encodeMessage(parsedMessage);
// Send the HelloWorld message by calling publishMessage on the
// wormhole core contract.
messageSequence = wormhole.publishMessage{value: wormholeFee}(
42000, // user specified message ID
encodedMessage,
wormholeFinality()
);
}
function receiveMessage(bytes memory encodedMessage) public {
// call the wormhole core contract to parse and verify the encodedMessage
(
IWormhole.VM memory wormholeMessage,
bool valid,
string memory reason
) = wormhole().parseAndVerifyVM(encodedMessage);
// confirm that the core layer verified the message
require(valid, reason);
// verify that this message was emitted by a trusted contract
require(verifyEmitter(wormholeMessage), "unknown emitter");
// decode the message payload into the HelloWorldStruct
HelloWorldMessage memory parsedMessage = decodeMessage(wormholeMessage.payload);
/**
Check to see if this message has been consumed already. If not,
save the parsed message in the receivedMessages mapping.
This check can protect against replay attacks in xDapps where messages are
only meant to be consumed once.
*/
require(!isMessageConsumed(wormholeMessage.hash), "message already consumed");
consumeMessage(wormholeMessage.hash, parsedMessage.message);
}
function registerEmitter(
uint16 emitterChainId,
bytes32 emitterAddress
) public onlyOwner {
// sanity check both input arguments
require(
emitterAddress != bytes32(0),
"emitterAddress cannot equal bytes32(0)"
);
require(
getRegisteredEmitter(emitterChainId) == bytes32(0),
"emitterChainId already registered"
);
// update the registeredEmitters state variable
setEmitter(emitterChainId, emitterAddress);
}
function verifyEmitter(IWormhole.VM memory vm) internal view returns (bool) {
// Verify that the sender of the wormhole message is a trusted
// HelloWorld contract.
if (getRegisteredEmitter(vm.emitterChainId) == vm.emitterAddress) {
return true;
}
return false;
}
modifier onlyOwner() {
require(owner() == msg.sender, "caller not the owner");
_;
}
}

View File

@ -1,48 +0,0 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "../libraries/BytesLib.sol";
import "./HelloWorldStructs.sol";
contract HelloWorldMessages is HelloWorldStructs {
using BytesLib for bytes;
function encodeMessage(
HelloWorldMessage memory parsedMessage
) public pure returns (bytes memory) {
// convert message string to bytes so that we can use the .length attribute
bytes memory encodedMessagePayload = abi.encodePacked(parsedMessage.message);
// return the encoded message
return abi.encodePacked(
parsedMessage.payloadID,
encodedMessagePayload.length,
encodedMessagePayload
);
}
function decodeMessage(
bytes memory encodedMessage
) public pure returns (HelloWorldMessage memory parsedMessage) {
// starting index for byte parsing
uint256 index = 0;
// parse and verify the payloadID
parsedMessage.payloadID = encodedMessage.toUint8(index);
require(parsedMessage.payloadID == 1, "invalid payloadID");
index += 1;
// parse the message string length
uint256 messageLength = encodedMessage.toUint256(index);
index += 32;
// parse the message string
bytes memory messageBytes = encodedMessage.slice(index, messageLength);
parsedMessage.message = string(messageBytes);
index += messageLength;
// confirm that the message was the expected length
require(index == encodedMessage.length, "invalid message length");
}
}

View File

@ -1,31 +0,0 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "./HelloWorldState.sol";
contract HelloWorldSetters is HelloWorldState {
function setOwner(address owner_) internal {
_state.owner = owner_;
}
function setWormhole(address wormhole_) internal {
_state.wormhole = payable(wormhole_);
}
function setChainId(uint16 chainId_) internal {
_state.chainId = chainId_;
}
function setWormholeFinality(uint8 finality) internal {
_state.wormholeFinality = finality;
}
function setEmitter(uint16 chainId, bytes32 emitter) internal {
_state.registeredEmitters[chainId] = emitter;
}
function consumeMessage(bytes32 hash, string memory message) internal {
_state.receivedMessages[hash] = message;
_state.consumedMessages[hash] = true;
}
}

View File

@ -1,37 +0,0 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {IWormhole} from "../interfaces/IWormhole.sol";
contract HelloWorldStorage {
struct State {
// owner of this contract
address owner;
// address of the Wormhole contract on this chain
address wormhole;
// Wormhole chain ID of this contract
uint16 chainId;
// The number of block confirmations needed before the wormhole network
// will attest a message.
uint8 wormholeFinality;
// Wormhole chain ID to known emitter address mapping. Xapps using
// Wormhole should register all deployed contracts on each chain to
// verify that messages being consumed are from trusted contracts.
mapping(uint16 => bytes32) registeredEmitters;
// verified message hash to received message mapping
mapping(bytes32 => string) receivedMessages;
// verified message hash to boolean
mapping(bytes32 => bool) consumedMessages;
}
}
contract HelloWorldState {
HelloWorldStorage.State _state;
}

View File

@ -1,11 +0,0 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
contract HelloWorldStructs {
struct HelloWorldMessage {
// unique identifier
uint8 payloadID;
// message payload (max size uint256)
string message;
}
}

View File

@ -0,0 +1,127 @@
// SPDX-License-Identifier: Apache 2
pragma solidity ^0.8.13;
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "../libraries/BytesLib.sol";
import {IWormhole} from "../interfaces/IWormhole.sol";
import "./CrossChainUSDCGovernance.sol";
import "./CrossChainUSDCMessages.sol";
contract CrossChainUSDC is CrossChainUSDCMessages, CrossChainUSDCGovernance, ReentrancyGuard {
using BytesLib for bytes;
function transferTokens(
address token,
uint256 amount,
uint16 toChain,
bytes32 mintRecipient
) public payable nonReentrant returns (uint64 messageSequence) {
// sanity check user input
require(amount > 0, "amount must be > 0");
require(toChain > 0, "invalid to chainId");
require(mintRecipient != bytes32(0), "invalid mint recipient");
// take custody of tokens
_custodyTokens(token, amount);
// cache wormhole instance and fees to save on gas
IWormhole wormhole = wormhole();
uint256 wormholeFee = wormhole.messageFee();
// Confirm that the caller has sent enough ether to pay for the wormhole
// message fee.
require(msg.value == wormholeFee, "insufficient value");
// cache Circle Bridge instance
ICircleBridge circleBridge = circleBridge();
// approve the USDC Bridge to spend tokens
SafeERC20.safeApprove(
IERC20(token),
address(circleBridge),
amount
);
// cache toChain information to save gas
uint32 targetChainDomain = getChainDomain(toChain);
bytes32 targetContract = getRegisteredEmitter(toChain);
// burn USDC on the bridge
uint64 nonce = circleBridge.depositForBurnWithCaller(
amount,
targetChainDomain,
mintRecipient,
token,
targetContract
);
// encode depositForBurn message
bytes memory encodedMessage = encodeDepositForBurnMessage(
DepositForBurn({
payloadId: uint8(1),
token: addressToBytes32(token),
amount: amount,
sourceDomain: getChainDomain(chainId()),
targetDomain: targetChainDomain,
nonce: nonce,
sender: addressToBytes32(address(this)),
mintRecipient: mintRecipient
})
);
// send the DepositForBurn wormhole message
messageSequence = wormhole.publishMessage{value : wormholeFee}(
0, // messageId, set to zero to opt out of batching
encodedMessage,
wormholeFinality()
);
}
function _custodyTokens(address token, uint256 amount) internal {
/// query own token balance before transfer
(,bytes memory queriedBalanceBefore) = token.staticcall(
abi.encodeWithSelector(IERC20.balanceOf.selector,
address(this))
);
uint256 balanceBefore = abi.decode(queriedBalanceBefore, (uint256));
/// deposit USDC/EUROC
SafeERC20.safeTransferFrom(
IERC20(token),
msg.sender,
address(this),
amount
);
/// query own token balance after transfer
(,bytes memory queriedBalanceAfter) = token.staticcall(
abi.encodeWithSelector(IERC20.balanceOf.selector,
address(this))
);
uint256 balanceAfter = abi.decode(queriedBalanceAfter, (uint256));
// This check is necessary until circle publishes the source code
// for the USDC Bridge.
require(
amount == balanceAfter - balanceBefore,
"USDC doesn't charge fees :/"
);
}
function verifyEmitter(IWormhole.VM memory vm) internal view returns (bool) {
// verify that the sender of the wormhole message is a trusted
if (getRegisteredEmitter(vm.emitterChainId) == vm.emitterAddress) {
return true;
}
return false;
}
function addressToBytes32(address address_) public pure returns (bytes32) {
return bytes32(uint256(uint160(address_)));
}
}

View File

@ -1,15 +1,24 @@
// SPDX-License-Identifier: UNLICENSED
// SPDX-License-Identifier: Apache 2
pragma solidity ^0.8.13;
import {IWormhole} from "../interfaces/IWormhole.sol";
import {ICircleBridge} from "../interfaces/circle/ICircleBridge.sol";
import "./HelloWorldSetters.sol";
import "./CrossChainUSDCSetters.sol";
contract HelloWorldGetters is HelloWorldSetters {
contract CrossChainUSDCGetters is CrossChainUSDCSetters {
function owner() public view returns (address) {
return _state.owner;
}
function pendingOwner() public view returns (address) {
return _state.pendingOwner;
}
function isInitialized(address impl) public view returns (bool) {
return _state.initializedImplementations[impl];
}
function wormhole() public view returns (IWormhole) {
return IWormhole(_state.wormhole);
}
@ -22,12 +31,16 @@ contract HelloWorldGetters is HelloWorldSetters {
return _state.wormholeFinality;
}
function circleBridge() public view returns (ICircleBridge) {
return ICircleBridge(_state.circleBridgeAddress);
}
function getRegisteredEmitter(uint16 emitterChainId) public view returns (bytes32) {
return _state.registeredEmitters[emitterChainId];
}
function getReceivedMessage(bytes32 hash) public view returns (string memory) {
return _state.receivedMessages[hash];
function getChainDomain(uint16 chainId_) public view returns (uint32) {
return _state.chainDomains[chainId_];
}
function isMessageConsumed(bytes32 hash) public view returns (bool) {

View File

@ -0,0 +1,112 @@
// SPDX-License-Identifier: Apache 2
pragma solidity ^0.8.13;
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Upgrade.sol";
import "./CrossChainUSDCSetters.sol";
import "./CrossChainUSDCGetters.sol";
import "./CrossChainUSDCState.sol";
contract CrossChainUSDCGovernance is CrossChainUSDCGetters, ERC1967Upgrade {
event ContractUpgraded(address indexed oldContract, address indexed newContract);
event WormholeFinalityUpdated(uint8 indexed oldLevel, uint8 indexed newFinality);
event OwnershipTransfered(address indexed oldOwner, address indexed newOwner);
/// @dev upgrade serves to upgrade contract implementations
function upgrade(uint16 chainId_, address newImplementation) public onlyOwner {
require(chainId_ == chainId(), "wrong chain");
address currentImplementation = _getImplementation();
_upgradeTo(newImplementation);
/// @dev call initialize function of the new implementation
(bool success, bytes memory reason) = newImplementation.delegatecall(
abi.encodeWithSignature("initialize()")
);
require(success, string(reason));
emit ContractUpgraded(currentImplementation, newImplementation);
}
/// @dev updateWormholeFinality serves to change the wormhole messaging consistencyLevel
function updateWormholeFinality(
uint16 chainId_,
uint8 newWormholeFinality
) public onlyOwner {
require(chainId_ == chainId(), "wrong chain");
require(newWormholeFinality > 0, "invalid wormhole finality");
uint8 currentWormholeFinality = wormholeFinality();
setWormholeFinality(newWormholeFinality);
emit WormholeFinalityUpdated(currentWormholeFinality, newWormholeFinality);
}
/**
* @dev submitOwnershipTransferRequest serves to begin the ownership transfer process of the contracts
* - it saves an address for the new owner in the pending state
*/
function submitOwnershipTransferRequest(
uint16 chainId_,
address newOwner
) public onlyOwner {
require(chainId_ == chainId(), "wrong chain");
require(newOwner != address(0), "newOwner cannot equal address(0)");
setPendingOwner(newOwner);
}
/**
* @dev confirmOwnershipTransferRequest serves to finalize an ownership transfer
* - it checks that the caller is the pendingOwner to validate the wallet address
* - it updates the owner state variable with the pendingOwner state variable
*/
function confirmOwnershipTransferRequest() public {
/// cache the new owner address
address newOwner = pendingOwner();
require(msg.sender == newOwner, "caller must be pendingOwner");
/// cache currentOwner for Event
address currentOwner = owner();
/// @dev update the owner in the contract state and reset the pending owner
setOwner(newOwner);
setPendingOwner(address(0));
emit OwnershipTransfered(currentOwner, newOwner);
}
/// @dev registerEmitter serves to save trusted emitter contract addresses
function registerEmitter(
uint16 emitterChainId,
bytes32 emitterAddress
) public onlyOwner {
// sanity check both input arguments
require(
emitterAddress != bytes32(0),
"emitterAddress cannot equal bytes32(0)"
);
require(
getRegisteredEmitter(emitterChainId) == bytes32(0),
"emitterChainId already registered"
);
// update the registeredEmitters state variable
setEmitter(emitterChainId, emitterAddress);
}
/// @dev registerChainDomain serves to save the USDC Bridge chain domains
function registerChainDomain(uint16 chainId_, uint32 domain) public onlyOwner {
// update the chainDomains state variable
setChainDomain(chainId_, domain);
}
modifier onlyOwner() {
require(owner() == msg.sender, "caller not the owner");
_;
}
}

View File

@ -0,0 +1,25 @@
// SPDX-License-Identifier: Apache 2
pragma solidity ^0.8.13;
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Upgrade.sol";
import "./CrossChainUSDC.sol";
contract CrossChainUSDCImplementation is CrossChainUSDC {
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);
_;
}
}

View File

@ -0,0 +1,67 @@
// SPDX-License-Identifier: Apache 2
pragma solidity ^0.8.13;
import "../libraries/BytesLib.sol";
import "./CrossChainUSDCStructs.sol";
contract CrossChainUSDCMessages is CrossChainUSDCStructs {
using BytesLib for bytes;
function encodeDepositForBurnMessage(
DepositForBurn memory message
) public pure returns (bytes memory) {
return abi.encodePacked(
uint8(1), // payloadId
message.token,
message.amount,
message.sourceDomain,
message.targetDomain,
message.nonce,
message.sender,
message.mintRecipient
);
}
function decodeDepositForBurnMessage(
bytes memory encoded
) public pure returns (DepositForBurn memory message) {
uint256 index = 0;
// payloadId
message.payloadId = encoded.toUint8(index);
index += 1;
require(message.payloadId == 1, "invalid message payloadId");
// token address, encoded as bytes32
message.token = encoded.toBytes32(index);
index += 32;
// amount burned
message.amount = encoded.toUint256(index);
index += 32;
// source domain
message.sourceDomain = encoded.toUint32(index);
index += 4;
// target domain
message.targetDomain = encoded.toUint32(index);
index += 4;
// nonce
message.nonce = encoded.toUint64(index);
index += 8;
// message sender
message.sender = encoded.toBytes32(index);
index += 32;
// mint recipient
message.mintRecipient = encoded.toBytes32(index);
index += 32;
require(index == encoded.length, "invalid message length");
}
}

View File

@ -0,0 +1,13 @@
// SPDX-License-Identifier: Apache 2
pragma solidity ^0.8.13;
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
contract CrossChainUSDCProxy is ERC1967Proxy {
constructor (address implementation, bytes memory initData)
ERC1967Proxy(
implementation,
initData
)
{}
}

View File

@ -0,0 +1,46 @@
// SPDX-License-Identifier: Apache 2
pragma solidity ^0.8.13;
import "./CrossChainUSDCState.sol";
contract CrossChainUSDCSetters is CrossChainUSDCState {
function setOwner(address owner_) internal {
_state.owner = owner_;
}
function setPendingOwner(address pendingOwner_) internal {
_state.pendingOwner = pendingOwner_;
}
function setInitialized(address implementatiom) internal {
_state.initializedImplementations[implementatiom] = true;
}
function setWormhole(address wormhole_) internal {
_state.wormhole = payable(wormhole_);
}
function setChainId(uint16 chainId_) internal {
_state.chainId = chainId_;
}
function setWormholeFinality(uint8 finality) internal {
_state.wormholeFinality = finality;
}
function setCircleBridge(address circleBridgeAddress_) internal {
_state.circleBridgeAddress = circleBridgeAddress_;
}
function setEmitter(uint16 chainId_, bytes32 emitter) internal {
_state.registeredEmitters[chainId_] = emitter;
}
function setChainDomain(uint16 chainId_, uint32 domain) internal {
_state.chainDomains[chainId_] = domain;
}
function consumeMessage(bytes32 hash) internal {
_state.consumedMessages[hash] = true;
}
}

View File

@ -0,0 +1,35 @@
// SPDX-License-Identifier: Apache 2
pragma solidity ^0.8.13;
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Upgrade.sol";
import {Context} from "@openzeppelin/contracts/utils/Context.sol";
import "./CrossChainUSDCSetters.sol";
contract CrossChainUSDCSetup is CrossChainUSDCSetters, ERC1967Upgrade, Context {
function setup(
address implementation,
uint16 chainId,
address wormhole,
uint8 finality,
address circleBridgeAddress
) public {
require(implementation != address(0), "invalid implementation");
require(chainId > 0, "invalid chainId");
require(wormhole != address(0), "invalid wormhole address");
require(circleBridgeAddress != address(0), "invalid USDC Bridge address");
setOwner(_msgSender());
setChainId(chainId);
setWormhole(wormhole);
setWormholeFinality(finality);
setCircleBridge(circleBridgeAddress);
// set the implementation
_upgradeTo(implementation);
// call initialize function of the new implementation
(bool success, bytes memory reason) = implementation.delegatecall(abi.encodeWithSignature("initialize()"));
require(success, string(reason));
}
}

View File

@ -0,0 +1,47 @@
// SPDX-License-Identifier: Apache 2
pragma solidity ^0.8.13;
import {IWormhole} from "../interfaces/IWormhole.sol";
contract CrossChainUSDCStorage {
struct State {
// Wormhole chain ID of this contract
uint16 chainId;
// The number of block confirmations needed before the wormhole network
// will attest a message.
uint8 wormholeFinality;
// owner of this contract
address owner;
// intermediate state when transfering contract ownership
address pendingOwner;
// address of the Wormhole contract on this chain
address wormhole;
// address of the trusted Circle Bridge contract on this chain
address circleBridgeAddress;
// mapping of initialized implementations
mapping(address => bool) initializedImplementations;
// Wormhole chain ID to known emitter address mapping
mapping(uint16 => bytes32) registeredEmitters;
// Wormhole chain ID to USDC Chain Domain Mapping
mapping(uint16 => uint32) chainDomains;
// verified message hash to boolean
mapping(bytes32 => bool) consumedMessages;
// storage gap
uint256[50] ______gap;
}
}
contract CrossChainUSDCState {
CrossChainUSDCStorage.State _state;
}

View File

@ -0,0 +1,15 @@
// SPDX-License-Identifier: Apache 2
pragma solidity ^0.8.13;
contract CrossChainUSDCStructs {
struct DepositForBurn {
uint8 payloadId; // == 1
bytes32 token;
uint256 amount;
uint32 sourceDomain;
uint32 targetDomain;
uint64 nonce;
bytes32 sender; // this contract
bytes32 mintRecipient;
}
}