-
Notifications
You must be signed in to change notification settings - Fork 54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: EVM changes required to support Solana #672
base: master
Are you sure you want to change the base?
Changes from all commits
6b4d8bc
c4153c4
9201079
5a54ddb
c338c54
aa36eb6
1afdf1e
7a85081
d94686d
3e7cb6c
18df9a0
231b509
75d1699
38be8e0
b223f6c
58b09d3
3d0e899
1ff7bb1
d36f9e6
848e043
3c2ab40
a89be73
656578a
56dca16
5c566fb
f80e188
740d44f
c9d0837
79bf5b8
aa48a3d
e582cb7
44bfd94
d1f7a3a
9905481
6e18091
48d65d4
0691f0a
9a93d49
0f2600f
fa137a2
0396330
8e8370f
af4c107
401e24c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,190 @@ | ||
// SPDX-License-Identifier: BUSL-1.1 | ||
pragma solidity ^0.8.0; | ||
|
||
import { IMessageTransmitter, ITokenMessenger } from "../external/interfaces/CCTPInterfaces.sol"; | ||
import { SpokePoolInterface } from "../interfaces/SpokePoolInterface.sol"; | ||
import { AdapterInterface } from "./interfaces/AdapterInterface.sol"; | ||
import { CircleCCTPAdapter, CircleDomainIds } from "../libraries/CircleCCTPAdapter.sol"; | ||
import { Bytes32ToAddress } from "../libraries/AddressConverters.sol"; | ||
|
||
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | ||
|
||
/** | ||
* @notice Contract containing logic to send messages from L1 to Solana via CCTP. | ||
* @dev Public functions calling external contracts do not guard against reentrancy because they are expected to be | ||
* called via delegatecall, which will execute this contract's logic within the context of the originating contract. | ||
* For example, the HubPool will delegatecall these functions, therefore it's only necessary that the HubPool's methods | ||
* that call this contract's logic guard against reentrancy. | ||
* @custom:security-contact [email protected] | ||
*/ | ||
|
||
// solhint-disable-next-line contract-name-camelcase | ||
contract Solana_Adapter is AdapterInterface, CircleCCTPAdapter { | ||
/** | ||
* @notice We use Bytes32ToAddress library to map a Solana address to an Ethereum address representation. | ||
* @dev The Ethereum address is derived from the Solana address by truncating it to its lowest 20 bytes. This same | ||
* conversion must be done by the HubPool owner when adding Solana spoke pool and setting the corresponding pool | ||
* rebalance and deposit routes. | ||
*/ | ||
using Bytes32ToAddress for bytes32; | ||
|
||
/** | ||
* @notice The official Circle CCTP MessageTransmitter contract endpoint. | ||
* @dev Posted officially here: https://developers.circle.com/stablecoins/docs/evm-smart-contracts | ||
*/ | ||
// solhint-disable-next-line immutable-vars-naming | ||
IMessageTransmitter public immutable cctpMessageTransmitter; | ||
|
||
// Solana spoke pool address, decoded from Base58 to bytes32. | ||
bytes32 public immutable SOLANA_SPOKE_POOL_BYTES32; | ||
|
||
// Solana spoke pool address, mapped to its EVM address representation. | ||
address public immutable SOLANA_SPOKE_POOL_ADDRESS; | ||
|
||
// USDC mint address on Solana, decoded from Base58 to bytes32. | ||
bytes32 public immutable SOLANA_USDC_BYTES32; | ||
|
||
// USDC mint address on Solana, mapped to its EVM address representation. | ||
address public immutable SOLANA_USDC_ADDRESS; | ||
|
||
// USDC token address on Solana for the spoke pool (vault ATA), decoded from Base58 to bytes32. | ||
bytes32 public immutable SOLANA_SPOKE_POOL_USDC_VAULT; | ||
|
||
// Custom errors for constructor argument validation. | ||
error InvalidCctpTokenMessenger(address tokenMessenger); | ||
error InvalidCctpMessageTransmitter(address messageTransmitter); | ||
|
||
// Custom errors for relayMessage validation. | ||
error InvalidRelayMessageTarget(address target); | ||
error InvalidOriginToken(address originToken); | ||
error InvalidDestinationChainId(uint256 destinationChainId); | ||
|
||
// Custom errors for relayTokens validation. | ||
error InvalidL1Token(address l1Token); | ||
error InvalidL2Token(address l2Token); | ||
error InvalidAmount(uint256 amount); | ||
error InvalidTokenRecipient(address to); | ||
|
||
/** | ||
* @notice Constructs new Adapter. | ||
* @param _l1Usdc USDC address on L1. | ||
* @param _cctpTokenMessenger TokenMessenger contract to bridge tokens via CCTP. | ||
* @param _cctpMessageTransmitter MessageTransmitter contract to bridge messages via CCTP. | ||
* @param solanaSpokePool Solana spoke pool address, decoded from Base58 to bytes32. | ||
* @param solanaUsdc USDC mint address on Solana, decoded from Base58 to bytes32. | ||
* @param solanaSpokePoolUsdcVault USDC token address on Solana for the spoke pool, decoded from Base58 to bytes32. | ||
*/ | ||
constructor( | ||
IERC20 _l1Usdc, | ||
ITokenMessenger _cctpTokenMessenger, | ||
IMessageTransmitter _cctpMessageTransmitter, | ||
bytes32 solanaSpokePool, | ||
bytes32 solanaUsdc, | ||
bytes32 solanaSpokePoolUsdcVault | ||
) CircleCCTPAdapter(_l1Usdc, _cctpTokenMessenger, CircleDomainIds.Solana) { | ||
// Solana adapter requires CCTP TokenMessenger and MessageTransmitter contracts to be set. | ||
if (address(_cctpTokenMessenger) == address(0)) { | ||
revert InvalidCctpTokenMessenger(address(_cctpTokenMessenger)); | ||
} | ||
if (address(_cctpMessageTransmitter) == address(0)) { | ||
revert InvalidCctpMessageTransmitter(address(_cctpMessageTransmitter)); | ||
} | ||
|
||
cctpMessageTransmitter = _cctpMessageTransmitter; | ||
|
||
SOLANA_SPOKE_POOL_BYTES32 = solanaSpokePool; | ||
SOLANA_SPOKE_POOL_ADDRESS = solanaSpokePool.toAddressUnchecked(); | ||
|
||
SOLANA_USDC_BYTES32 = solanaUsdc; | ||
SOLANA_USDC_ADDRESS = solanaUsdc.toAddressUnchecked(); | ||
|
||
SOLANA_SPOKE_POOL_USDC_VAULT = solanaSpokePoolUsdcVault; | ||
} | ||
|
||
/** | ||
* @notice Send cross-chain message to target on Solana. | ||
* @dev Only allows sending messages to the Solana spoke pool. | ||
* @param target Program on Solana (translated as EVM address) that will receive message. | ||
* @param message Data to send to target. | ||
*/ | ||
function relayMessage(address target, bytes calldata message) external payable override { | ||
if (target != SOLANA_SPOKE_POOL_ADDRESS) { | ||
revert InvalidRelayMessageTarget(target); | ||
} | ||
|
||
bytes4 selector = bytes4(message[:4]); | ||
if (selector == SpokePoolInterface.setEnableRoute.selector) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need to keep the address version of setEnableRoute for backward compatibility with the hub pool, and that specific handler translates the message to the bytes32 version. @Reinis-FRP, do you remember why we decided to handle the translation here, which led us to do the casting here instead of handling it in SVM? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The main reason is that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But @Reinis-FRP , couldn’t this conversion also be handled on the svm side? We initially chose to implement it here, but after thinking it over, I agree with @mrice32 that it might make more sense to handle this directly within the solana specific implementation. Also, @mrice32 pointed out a similar approach for Arbitrum in this comment. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This would be bit tricky as we would need to pass mapping account that stores address translation when receiving the message on Solana side. It does not really belong under Not saying its impossible to do, but it seemed to have less code complexity if we move the translation to the adapter on EVM side, especially, since it already needs to translate trimmed Solana USDC address to its vault associated token account anyway. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So it sounds like the tradeoff here is (less code complexity, special case for |
||
cctpMessageTransmitter.sendMessage( | ||
CircleDomainIds.Solana, | ||
SOLANA_SPOKE_POOL_BYTES32, | ||
_translateSetEnableRoute(message) | ||
); | ||
} else { | ||
cctpMessageTransmitter.sendMessage(CircleDomainIds.Solana, SOLANA_SPOKE_POOL_BYTES32, message); | ||
} | ||
|
||
// TODO: consider if we need also to emit the translated message. | ||
emit MessageRelayed(target, message); | ||
} | ||
|
||
/** | ||
* @notice Bridge tokens to Solana. | ||
* @dev Only allows bridging USDC to Solana spoke pool. | ||
* @param l1Token L1 token to deposit. | ||
* @param l2Token L2 token to receive. | ||
* @param amount Amount of L1 tokens to deposit and L2 tokens to receive. | ||
* @param to Bridge recipient. | ||
*/ | ||
function relayTokens( | ||
address l1Token, | ||
address l2Token, | ||
uint256 amount, | ||
address to | ||
) external payable override { | ||
if (l1Token != address(usdcToken)) { | ||
revert InvalidL1Token(l1Token); | ||
} | ||
if (l2Token != SOLANA_USDC_ADDRESS) { | ||
revert InvalidL2Token(l2Token); | ||
} | ||
if (amount > type(uint64).max) { | ||
revert InvalidAmount(amount); | ||
} | ||
if (to != SOLANA_SPOKE_POOL_ADDRESS) { | ||
revert InvalidTokenRecipient(to); | ||
} | ||
|
||
_transferUsdc(SOLANA_SPOKE_POOL_USDC_VAULT, amount); | ||
|
||
// TODO: consider if we need also to emit the translated addresses. | ||
emit TokensRelayed(l1Token, l2Token, amount, to); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it could be useful to emit the translated addresses at very little additional marginal cost |
||
} | ||
|
||
/** | ||
* @notice Translates a message to enable/disable a route on Solana spoke pool. | ||
* @param message Message to translate, expecting setEnableRoute(address,uint256,bool). | ||
* @return Translated message, using setEnableRoute(bytes32,uint64,bool). | ||
*/ | ||
function _translateSetEnableRoute(bytes calldata message) internal view returns (bytes memory) { | ||
(address originToken, uint256 destinationChainId, bool enable) = abi.decode( | ||
message[4:], | ||
(address, uint256, bool) | ||
); | ||
|
||
if (originToken != SOLANA_USDC_ADDRESS) { | ||
revert InvalidOriginToken(originToken); | ||
} | ||
|
||
if (destinationChainId > type(uint64).max) { | ||
revert InvalidDestinationChainId(destinationChainId); | ||
} | ||
|
||
return | ||
abi.encodeWithSignature( | ||
"setEnableRoute(bytes32,uint64,bool)", | ||
SOLANA_USDC_BYTES32, | ||
uint64(destinationChainId), | ||
enable | ||
); | ||
Comment on lines
+182
to
+188
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OOC, why not do this mapping on the Solana side? In theory, could we not have a mapping (from truncated address to full address) exist on the Solana side that could be modified by an admin call. Then, the flow to add a new token would require no contract changes:
Side note: this is somewhat similar to something we have to do on arbitrum. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We still need to map truncated USDC address to Solana spoke pool vault here to support |
||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How much of this contract is specific to Solana vs could be used with any chain we connect to via CCTP?
I haven't gone deep on the specifics, but, for instance, can we build an adapter that could be used just as well with an EVM chain like BSC?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is quite specific in a sense that it must be aware of Solana token model where recipient SpokePool address needs to be translated to its corresponding vault token address.