wormhole-circle-integration/evm/src/cross_chain_usdc/CrossChainUSDC.sol

293 lines
11 KiB
Solidity

// 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";
/// @notice These contracts burn and mint USDC by using Circle's Cross-Chain Transfer Protocol allowing
/// for seemless cross-chain USDC transfers. They also emit Wormhole messages that contain instructions
/// describing what to do with the USDC on the target chain.
contract CrossChainUSDC is CrossChainUSDCMessages, CrossChainUSDCGovernance, ReentrancyGuard {
using BytesLib for bytes;
/// @dev `transferTokens` calls the Circle Bridge contract to burn USDC, and emits
/// a Wormhole message with information about the cross-chain trasnfer.
function transferTokens(
address token,
uint256 amount,
uint16 targetChain,
bytes32 mintRecipient
) public payable nonReentrant 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");
// call the Circle Bridge and depositForBurn
uint64 nonce = _transferTokens(token, amount, targetChain, mintRecipient);
// encode depositForBurn message
bytes memory encodedMessage = encodeWormholeDeposit(
WormholeDeposit({
payloadId: uint8(1),
token: addressToBytes32(token),
amount: amount,
sourceDomain: getChainDomain(chainId()),
targetDomain: getChainDomain(targetChain),
nonce: nonce,
circleSender: addressToBytes32(address(circleBridge()))
})
);
// send the DepositForBurn wormhole message
messageSequence = wormhole.publishMessage{value : wormholeFee}(
0, // messageId, set to zero to opt out of batching
encodedMessage,
wormholeFinality()
);
}
/// @dev `transferTokensWithPayload` calls the Circle Bridge contract to burn USDC. It emits
/// a Wormhole message containing a user-specified payload with instructions for what to do with
/// the USDC once it has been minted on the target chain.
function transferTokensWithPayload(
address token,
uint256 amount,
uint16 targetChain,
bytes32 mintRecipient,
bytes memory payload
) public payable nonReentrant 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");
// Call the circle bridge and depositForBurn. The mintRecipient
// should be the target contract composing on this USDC integration.
uint64 nonce = _transferTokens(token, amount, targetChain, mintRecipient);
// depositForBurn deposit message header
WormholeDeposit memory depositHeader = WormholeDeposit({
payloadId: uint8(2),
token: addressToBytes32(token),
amount: amount,
sourceDomain: getChainDomain(chainId()),
targetDomain: getChainDomain(targetChain),
nonce: nonce,
circleSender: addressToBytes32(address(circleBridge()))
});
// encode depositForBurn message
bytes memory encodedMessage = encodeWormholeDepositWithPayload(
WormholeDepositWithPayload({
depositHeader: depositHeader,
mintRecipient: mintRecipient,
payload: payload
})
);
// send the DepositForBurn wormhole message
messageSequence = wormhole.publishMessage{value : wormholeFee}(
0, // messageId, set to zero to opt out of batching
encodedMessage,
wormholeFinality()
);
}
function _transferTokens(
address token,
uint256 amount,
uint16 targetChain,
bytes32 mintRecipient
) internal returns (uint64 nonce) {
// sanity check user input
require(amount > 0, "amount must be > 0");
require(targetChain > 0, "invalid to chainId");
require(mintRecipient != bytes32(0), "invalid mint recipient");
// take custody of tokens
custodyTokens(token, amount);
// cache Circle Bridge instance
ICircleBridge circleBridge = circleBridge();
// approve the USDC Bridge to spend tokens
SafeERC20.safeApprove(
IERC20(token),
address(circleBridge),
amount
);
// confirm that the target contract is registered
require(getRegisteredEmitter(targetChain) != bytes32(0), "target contract not registered");
// burn USDC on the bridge
nonce = circleBridge.depositForBurnWithCaller(
amount,
getChainDomain(targetChain),
mintRecipient,
token,
getRegisteredEmitter(targetChain)
);
}
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,
"fee-on-transfer tokens not permitted"
);
}
/// @dev `custodyTokens` verifies the Wormhole message from the source chain and
/// verifies that the passed Circle Bridge message is valid. It calls the Circle Bridge
/// contract by passing the Circle message and attestation to mint tokens to
/// the specified mint recipient.
function redeemTokens(RedeemParameters memory params) public {
// verify the wormhole message
IWormhole.VM memory verifiedMessage = verifyWormholeRedeemMessage(
params.encodedWormholeMessage
);
// decode the message payload into the WormholeDeposit struct
WormholeDeposit memory wormholeDeposit = decodeWormholeDeposit(
verifiedMessage.payload
);
// parse the circle bridge message
CircleDeposit memory circleDeposit = decodeCircleDeposit(
params.circleBridgeMessage
);
// confirm that the caller passed the correct message pair
require(verifyCircleMessage(wormholeDeposit, circleDeposit), "invalid message pair");
// call the circle bridge to mint tokens to the recipient
bool success = circleTransmitter().receiveMessage(
params.circleBridgeMessage,
params.circleAttestation
);
require(success, "failed to mint USDC");
}
/// @dev `custodyTokensWithPayload` verifies the Wormhole message from the source chain and
/// verifies that the passed Circle Bridge message is valid. It calls the Circle Bridge
/// contract by passing the Circle message and attestation to mint tokens to
/// the specified mint recipient. It also verifies that the caller is the specified mint
/// recipient to ensure atomic execution of the additional instructions in the Wormhole message.
function redeemTokensWithPayload(
RedeemParameters memory params
) public returns (WormholeDepositWithPayload memory wormholeDepositWithPayload) {
// verify the wormhole message
IWormhole.VM memory verifiedMessage = verifyWormholeRedeemMessage(
params.encodedWormholeMessage
);
// decode the message payload into the WormholeDeposit struct
wormholeDepositWithPayload = decodeWormholeDepositWithPayload(
verifiedMessage.payload
);
// confirm that the caller is the mint recipient to ensure atomic execution
require(
addressToBytes32(msg.sender) == wormholeDepositWithPayload.mintRecipient,
"caller must be mintRecipient"
);
// parse the circle bridge message
CircleDeposit memory circleDeposit = decodeCircleDeposit(
params.circleBridgeMessage
);
// confirm that the caller passed the correct message pair
require(verifyCircleMessage(wormholeDepositWithPayload.depositHeader, circleDeposit), "invalid message pair");
// call the circle bridge to mint tokens to the recipient
bool success = circleTransmitter().receiveMessage(
params.circleBridgeMessage,
params.circleAttestation
);
require(success, "failed to mint USDC");
}
function verifyWormholeRedeemMessage(
bytes memory encodedMessage
) internal returns (IWormhole.VM memory) {
// parse and verify the Wormhole core message
(
IWormhole.VM memory verifiedMessage,
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(verifiedMessage), "unknown emitter");
// revert if this message has been consumed already
require(!isMessageConsumed(verifiedMessage.hash), "message already consumed");
consumeMessage(verifiedMessage.hash);
return verifiedMessage;
}
function verifyEmitter(IWormhole.VM memory vm) internal view returns (bool) {
// verify that the sender of the wormhole message is a trusted
return (getRegisteredEmitter(vm.emitterChainId) == vm.emitterAddress);
}
function verifyCircleMessage(
WormholeDeposit memory wormhole,
CircleDeposit memory circle
) internal view returns (bool) {
return (
wormhole.sourceDomain == circle.sourceDomain &&
wormhole.targetDomain == circle.targetDomain &&
wormhole.nonce == circle.nonce &&
wormhole.circleSender == circle.circleSender &&
addressToBytes32(address(circleBridge())) == circle.circleReceiver
);
}
function addressToBytes32(address address_) public pure returns (bytes32) {
return bytes32(uint256(uint160(address_)));
}
}