-
Notifications
You must be signed in to change notification settings - Fork 75
feat(chain-adapters): add solana adapter #641
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
Changes from all commits
f6e3db8
ce0ceb4
7f3746c
d325c92
fd0d056
3a8a728
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,193 @@ | ||
| // 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 { 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 bugs@across.to | ||
| */ | ||
|
|
||
| // solhint-disable-next-line contract-name-camelcase | ||
| contract Solana_Adapter is AdapterInterface, CircleCCTPAdapter { | ||
| /** | ||
| * @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; | ||
|
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 more belongs to CircleCCTPAdapter, though that would require passing it in constructor for all other adapters that don't use CCTP for general message transmission. |
||
|
|
||
| // 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); | ||
|
Comment on lines
+49
to
+57
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 used these for debugging, though HubPool does not bubble up revert data on delegatecall. Shall we still keep custom errors or revert with empty data at the adapter? 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 they are ok to have adapter specific errors? if there are other errors that other adapters re-use we should cleary use them but things like |
||
|
|
||
| /** | ||
| * @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 | ||
|
Comment on lines
+72
to
+74
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. nit: it might be easier/cleaner/simpler/more consistant to just have these hard coded as constants, as done in some of the other adapters. 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 left them as immutables so that its easier to test integration flow on testnets that would have different USDC and we don't have a proper authority for the spoke yet. 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. right. I dont think we're going to be doing too much testnet work (mainly better for a lot of these flows to test directly on mainnet) but fair enough that it makes it easier to change. |
||
| ) 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 = _trimSolanaAddress(solanaSpokePool); | ||
|
|
||
| SOLANA_USDC_BYTES32 = solanaUsdc; | ||
| SOLANA_USDC_ADDRESS = _trimSolanaAddress(solanaUsdc); | ||
|
|
||
| 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 { | ||
|
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. Do we need permission checks here? 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 is delegatecalled, so we check CCTP sender on Solana side matches HubPool |
||
| if (target != SOLANA_SPOKE_POOL_ADDRESS) { | ||
|
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. what is the downside with letting this target non spoke pool address? 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. Having it hardcoded potentially limits flexibility for future updates or use cases. Maybe we can add a setter or remove the check if not strictly required as @chrismaree is exploring. 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 accepts target address that must be translated to bytes32, but since we don't have translation mapping for any other target then obviously the caller has been misconfigured. As for updates, I understand the general pattern for these adapters is to have them immutable and upgrade by redeploying and updating the instance in HubPool. 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. @Reinis-FRP that's a good point. this is not a generic adapter as a result of the encoding so there is no situation where you should ever be passing in anything other than a known target address. if you are, you're doing something wrong and it should error. I agree with leaving it like this. |
||
| 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. this is great. it's nice that we only need to worry about one selector as the others are bool. |
||
| 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) { | ||
|
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. similar comment to before: if the I agree constraining the execution flow is properly better but in theory there is no issue not having this require, right? 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 don't have recipient ATA for any other than spoke. If this does not match then the caller has been misconfigured, so better to block it here. |
||
| 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); | ||
| } | ||
|
|
||
| /** | ||
| * @notice Helper 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. | ||
| * @param solanaAddress Solana address (Base58 decoded to bytes32) to map to its Ethereum address representation. | ||
| * @return Ethereum address representation of the Solana address. | ||
| */ | ||
| function _trimSolanaAddress(bytes32 solanaAddress) internal pure returns (address) { | ||
| return address(uint160(uint256(solanaAddress))); | ||
| } | ||
|
|
||
| /** | ||
| * @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 | ||
| ); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
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.
nit: is this a "Solana_Adapter" or it is better called "Cctp_Adapter"?
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.
This includes logic specific to Solana, e.g. how
setEnableRouteis translated. I was though considering to movecctpMessageTransmitterto baseCircleCCTPAdapter, but that would have required adding it to constructor for all other adapters that don't use CCTP for general message transmission.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.
that's fair enough. given we already have the
CircleCCTPAdapterwithin this repo I agree this naming structure makes the most sence.