Skip to content

Commit 6b4d8bc

Browse files
feat(chain-adapters): add solana adapter (#641)
* feat(chain-adapters): add solana adapter Signed-off-by: Reinis Martinsons <[email protected]> * fix: comments Signed-off-by: Reinis Martinsons <[email protected]> * test: solana adapter Signed-off-by: Reinis Martinsons <[email protected]> * Update contracts/chain-adapters/Solana_Adapter.sol Co-authored-by: Chris Maree <[email protected]> * fix: do not hash bytes32 svm address Signed-off-by: Reinis Martinsons <[email protected]> --------- Signed-off-by: Reinis Martinsons <[email protected]> Co-authored-by: Chris Maree <[email protected]>
1 parent 7d3eab1 commit 6b4d8bc

File tree

7 files changed

+398
-6
lines changed

7 files changed

+398
-6
lines changed
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
pragma solidity ^0.8.0;
3+
4+
import { IMessageTransmitter, ITokenMessenger } from "../external/interfaces/CCTPInterfaces.sol";
5+
import { SpokePoolInterface } from "../interfaces/SpokePoolInterface.sol";
6+
import { AdapterInterface } from "./interfaces/AdapterInterface.sol";
7+
import { CircleCCTPAdapter, CircleDomainIds } from "../libraries/CircleCCTPAdapter.sol";
8+
9+
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
10+
11+
/**
12+
* @notice Contract containing logic to send messages from L1 to Solana via CCTP.
13+
* @dev Public functions calling external contracts do not guard against reentrancy because they are expected to be
14+
* called via delegatecall, which will execute this contract's logic within the context of the originating contract.
15+
* For example, the HubPool will delegatecall these functions, therefore it's only necessary that the HubPool's methods
16+
* that call this contract's logic guard against reentrancy.
17+
* @custom:security-contact [email protected]
18+
*/
19+
20+
// solhint-disable-next-line contract-name-camelcase
21+
contract Solana_Adapter is AdapterInterface, CircleCCTPAdapter {
22+
/**
23+
* @notice The official Circle CCTP MessageTransmitter contract endpoint.
24+
* @dev Posted officially here: https://developers.circle.com/stablecoins/docs/evm-smart-contracts
25+
*/
26+
// solhint-disable-next-line immutable-vars-naming
27+
IMessageTransmitter public immutable cctpMessageTransmitter;
28+
29+
// Solana spoke pool address, decoded from Base58 to bytes32.
30+
bytes32 public immutable SOLANA_SPOKE_POOL_BYTES32;
31+
32+
// Solana spoke pool address, mapped to its EVM address representation.
33+
address public immutable SOLANA_SPOKE_POOL_ADDRESS;
34+
35+
// USDC mint address on Solana, decoded from Base58 to bytes32.
36+
bytes32 public immutable SOLANA_USDC_BYTES32;
37+
38+
// USDC mint address on Solana, mapped to its EVM address representation.
39+
address public immutable SOLANA_USDC_ADDRESS;
40+
41+
// USDC token address on Solana for the spoke pool (vault ATA), decoded from Base58 to bytes32.
42+
bytes32 public immutable SOLANA_SPOKE_POOL_USDC_VAULT;
43+
44+
// Custom errors for constructor argument validation.
45+
error InvalidCctpTokenMessenger(address tokenMessenger);
46+
error InvalidCctpMessageTransmitter(address messageTransmitter);
47+
48+
// Custom errors for relayMessage validation.
49+
error InvalidRelayMessageTarget(address target);
50+
error InvalidOriginToken(address originToken);
51+
error InvalidDestinationChainId(uint256 destinationChainId);
52+
53+
// Custom errors for relayTokens validation.
54+
error InvalidL1Token(address l1Token);
55+
error InvalidL2Token(address l2Token);
56+
error InvalidAmount(uint256 amount);
57+
error InvalidTokenRecipient(address to);
58+
59+
/**
60+
* @notice Constructs new Adapter.
61+
* @param _l1Usdc USDC address on L1.
62+
* @param _cctpTokenMessenger TokenMessenger contract to bridge tokens via CCTP.
63+
* @param _cctpMessageTransmitter MessageTransmitter contract to bridge messages via CCTP.
64+
* @param solanaSpokePool Solana spoke pool address, decoded from Base58 to bytes32.
65+
* @param solanaUsdc USDC mint address on Solana, decoded from Base58 to bytes32.
66+
* @param solanaSpokePoolUsdcVault USDC token address on Solana for the spoke pool, decoded from Base58 to bytes32.
67+
*/
68+
constructor(
69+
IERC20 _l1Usdc,
70+
ITokenMessenger _cctpTokenMessenger,
71+
IMessageTransmitter _cctpMessageTransmitter,
72+
bytes32 solanaSpokePool,
73+
bytes32 solanaUsdc,
74+
bytes32 solanaSpokePoolUsdcVault
75+
) CircleCCTPAdapter(_l1Usdc, _cctpTokenMessenger, CircleDomainIds.Solana) {
76+
// Solana adapter requires CCTP TokenMessenger and MessageTransmitter contracts to be set.
77+
if (address(_cctpTokenMessenger) == address(0)) {
78+
revert InvalidCctpTokenMessenger(address(_cctpTokenMessenger));
79+
}
80+
if (address(_cctpMessageTransmitter) == address(0)) {
81+
revert InvalidCctpMessageTransmitter(address(_cctpMessageTransmitter));
82+
}
83+
84+
cctpMessageTransmitter = _cctpMessageTransmitter;
85+
86+
SOLANA_SPOKE_POOL_BYTES32 = solanaSpokePool;
87+
SOLANA_SPOKE_POOL_ADDRESS = _trimSolanaAddress(solanaSpokePool);
88+
89+
SOLANA_USDC_BYTES32 = solanaUsdc;
90+
SOLANA_USDC_ADDRESS = _trimSolanaAddress(solanaUsdc);
91+
92+
SOLANA_SPOKE_POOL_USDC_VAULT = solanaSpokePoolUsdcVault;
93+
}
94+
95+
/**
96+
* @notice Send cross-chain message to target on Solana.
97+
* @dev Only allows sending messages to the Solana spoke pool.
98+
* @param target Program on Solana (translated as EVM address) that will receive message.
99+
* @param message Data to send to target.
100+
*/
101+
function relayMessage(address target, bytes calldata message) external payable override {
102+
if (target != SOLANA_SPOKE_POOL_ADDRESS) {
103+
revert InvalidRelayMessageTarget(target);
104+
}
105+
106+
bytes4 selector = bytes4(message[:4]);
107+
if (selector == SpokePoolInterface.setEnableRoute.selector) {
108+
cctpMessageTransmitter.sendMessage(
109+
CircleDomainIds.Solana,
110+
SOLANA_SPOKE_POOL_BYTES32,
111+
_translateSetEnableRoute(message)
112+
);
113+
} else {
114+
cctpMessageTransmitter.sendMessage(CircleDomainIds.Solana, SOLANA_SPOKE_POOL_BYTES32, message);
115+
}
116+
117+
// TODO: consider if we need also to emit the translated message.
118+
emit MessageRelayed(target, message);
119+
}
120+
121+
/**
122+
* @notice Bridge tokens to Solana.
123+
* @dev Only allows bridging USDC to Solana spoke pool.
124+
* @param l1Token L1 token to deposit.
125+
* @param l2Token L2 token to receive.
126+
* @param amount Amount of L1 tokens to deposit and L2 tokens to receive.
127+
* @param to Bridge recipient.
128+
*/
129+
function relayTokens(
130+
address l1Token,
131+
address l2Token,
132+
uint256 amount,
133+
address to
134+
) external payable override {
135+
if (l1Token != address(usdcToken)) {
136+
revert InvalidL1Token(l1Token);
137+
}
138+
if (l2Token != SOLANA_USDC_ADDRESS) {
139+
revert InvalidL2Token(l2Token);
140+
}
141+
if (amount > type(uint64).max) {
142+
revert InvalidAmount(amount);
143+
}
144+
if (to != SOLANA_SPOKE_POOL_ADDRESS) {
145+
revert InvalidTokenRecipient(to);
146+
}
147+
148+
_transferUsdc(SOLANA_SPOKE_POOL_USDC_VAULT, amount);
149+
150+
// TODO: consider if we need also to emit the translated addresses.
151+
emit TokensRelayed(l1Token, l2Token, amount, to);
152+
}
153+
154+
/**
155+
* @notice Helper to map a Solana address to an Ethereum address representation.
156+
* @dev The Ethereum address is derived from the Solana address by truncating it to its lowest 20 bytes. This same
157+
* conversion must be done by the HubPool owner when adding Solana spoke pool and setting the corresponding pool
158+
* rebalance and deposit routes.
159+
* @param solanaAddress Solana address (Base58 decoded to bytes32) to map to its Ethereum address representation.
160+
* @return Ethereum address representation of the Solana address.
161+
*/
162+
function _trimSolanaAddress(bytes32 solanaAddress) internal pure returns (address) {
163+
return address(uint160(uint256(solanaAddress)));
164+
}
165+
166+
/**
167+
* @notice Translates a message to enable/disable a route on Solana spoke pool.
168+
* @param message Message to translate, expecting setEnableRoute(address,uint256,bool).
169+
* @return Translated message, using setEnableRoute(bytes32,uint64,bool).
170+
*/
171+
function _translateSetEnableRoute(bytes calldata message) internal view returns (bytes memory) {
172+
(address originToken, uint256 destinationChainId, bool enable) = abi.decode(
173+
message[4:],
174+
(address, uint256, bool)
175+
);
176+
177+
if (originToken != SOLANA_USDC_ADDRESS) {
178+
revert InvalidOriginToken(originToken);
179+
}
180+
181+
if (destinationChainId > type(uint64).max) {
182+
revert InvalidDestinationChainId(destinationChainId);
183+
}
184+
185+
return
186+
abi.encodeWithSignature(
187+
"setEnableRoute(bytes32,uint64,bool)",
188+
SOLANA_USDC_BYTES32,
189+
uint64(destinationChainId),
190+
enable
191+
);
192+
}
193+
}

contracts/external/interfaces/CCTPInterfaces.sol

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,24 @@ interface ITokenMinter {
7474
*/
7575
function burnLimitsPerMessage(address token) external view returns (uint256);
7676
}
77+
78+
/**
79+
* IMessageTransmitter in CCTP inherits IRelayer and IReceiver, but here we only import sendMessage from IRelayer:
80+
* https://github.com/circlefin/evm-cctp-contracts/blob/377c9bd813fb86a42d900ae4003599d82aef635a/src/interfaces/IMessageTransmitter.sol#L25
81+
* https://github.com/circlefin/evm-cctp-contracts/blob/377c9bd813fb86a42d900ae4003599d82aef635a/src/interfaces/IRelayer.sol#L23-L35
82+
*/
83+
interface IMessageTransmitter {
84+
/**
85+
* @notice Sends an outgoing message from the source domain.
86+
* @dev Increment nonce, format the message, and emit `MessageSent` event with message information.
87+
* @param destinationDomain Domain of destination chain
88+
* @param recipient Address of message recipient on destination domain as bytes32
89+
* @param messageBody Raw bytes content of message
90+
* @return nonce reserved by message
91+
*/
92+
function sendMessage(
93+
uint32 destinationDomain,
94+
bytes32 recipient,
95+
bytes calldata messageBody
96+
) external returns (uint64);
97+
}

contracts/libraries/CircleCCTPAdapter.sol

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ library CircleDomainIds {
99
uint32 public constant Ethereum = 0;
1010
uint32 public constant Optimism = 2;
1111
uint32 public constant Arbitrum = 3;
12+
uint32 public constant Solana = 5;
1213
uint32 public constant Base = 6;
1314
uint32 public constant Polygon = 7;
1415
// Use this value for placeholder purposes only for adapters that extend this adapter but haven't yet been
@@ -87,17 +88,26 @@ abstract contract CircleCCTPAdapter {
8788
* @param amount Amount of USDC to transfer.
8889
*/
8990
function _transferUsdc(address to, uint256 amount) internal {
91+
_transferUsdc(_addressToBytes32(to), amount);
92+
}
93+
94+
/**
95+
* @notice Transfers USDC from the current domain to the given address on the new domain.
96+
* @dev This function will revert if the CCTP bridge is disabled. I.e. if the zero address is passed to the constructor for the cctpTokenMessenger.
97+
* @param to Address to receive USDC on the new domain represented as bytes32.
98+
* @param amount Amount of USDC to transfer.
99+
*/
100+
function _transferUsdc(bytes32 to, uint256 amount) internal {
90101
// Only approve the exact amount to be transferred
91102
usdcToken.safeIncreaseAllowance(address(cctpTokenMessenger), amount);
92103
// Submit the amount to be transferred to bridged via the TokenMessenger.
93104
// If the amount to send exceeds the burn limit per message, then split the message into smaller parts.
94105
ITokenMinter cctpMinter = cctpTokenMessenger.localMinter();
95106
uint256 burnLimit = cctpMinter.burnLimitsPerMessage(address(usdcToken));
96107
uint256 remainingAmount = amount;
97-
bytes32 recipient = _addressToBytes32(to);
98108
while (remainingAmount > 0) {
99109
uint256 partAmount = remainingAmount > burnLimit ? burnLimit : remainingAmount;
100-
cctpTokenMessenger.depositForBurn(partAmount, recipientCircleDomainId, recipient, address(usdcToken));
110+
cctpTokenMessenger.depositForBurn(partAmount, recipientCircleDomainId, to, address(usdcToken));
101111
remainingAmount -= partAmount;
102112
}
103113
}

test/evm/hardhat/MerkleLib.utils.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
BigNumber,
55
defaultAbiCoder,
66
keccak256,
7-
toBNWei,
7+
toBNWeiWithDecimals,
88
createRandomBytes32,
99
Contract,
1010
} from "../../../utils/utils";
@@ -119,9 +119,14 @@ export async function constructSingleRelayerRefundTree(l2Token: Contract | Strin
119119
return { leaves, tree };
120120
}
121121

122-
export async function constructSingleChainTree(token: string, scalingSize = 1, repaymentChain = repaymentChainId) {
123-
const tokensSendToL2 = toBNWei(100 * scalingSize);
124-
const realizedLpFees = toBNWei(10 * scalingSize);
122+
export async function constructSingleChainTree(
123+
token: string,
124+
scalingSize = 1,
125+
repaymentChain = repaymentChainId,
126+
decimals = 18
127+
) {
128+
const tokensSendToL2 = toBNWeiWithDecimals(100 * scalingSize, decimals);
129+
const realizedLpFees = toBNWeiWithDecimals(10 * scalingSize, decimals);
125130
const leaves = buildPoolRebalanceLeaves(
126131
[repaymentChain], // repayment chain. In this test we only want to send one token to one chain.
127132
[[token]], // l1Token. We will only be sending 1 token to one chain.

0 commit comments

Comments
 (0)