From f6e3db874eb5ab9bc229c7981735619cf741879e Mon Sep 17 00:00:00 2001 From: Reinis Martinsons Date: Wed, 9 Oct 2024 14:37:32 +0000 Subject: [PATCH 1/5] feat(chain-adapters): add solana adapter Signed-off-by: Reinis Martinsons --- contracts/chain-adapters/Solana_Adapter.sol | 174 ++++++++++++++++++ .../external/interfaces/CCTPInterfaces.sol | 21 +++ contracts/libraries/CircleCCTPAdapter.sol | 14 +- 3 files changed, 207 insertions(+), 2 deletions(-) create mode 100644 contracts/chain-adapters/Solana_Adapter.sol diff --git a/contracts/chain-adapters/Solana_Adapter.sol b/contracts/chain-adapters/Solana_Adapter.sol new file mode 100644 index 000000000..11223f42c --- /dev/null +++ b/contracts/chain-adapters/Solana_Adapter.sol @@ -0,0 +1,174 @@ +// 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. + * @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; + + bytes32 public immutable SOLANA_SPOKE_POOL_BYTES32; + address public immutable SOLANA_SPOKE_POOL_ADDRESS; + + bytes32 public immutable SOLANA_USDC_BYTES32; + address public immutable SOLANA_USDC_ADDRESS; + + bytes32 public immutable SOLANA_SPOKE_POOL_USDC_VAULT; + + error InvalidCctpTokenMessenger(address tokenMessenger); + error InvalidCctpMessageTransmitter(address messageTransmitter); + + error InvalidRelayMessageTarget(address target); + error InvalidOriginToken(address originToken); + error InvalidDestinationChainId(uint256 destinationChainId); + + error InvalidL1Token(address l1Token); + error InvalidL2Token(address l2Token); + error InvalidAmount(uint256 amount); + error InvalidTokenRecipient(address to); + + 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 = _mapSolanaAddress(solanaSpokePool); + + SOLANA_USDC_BYTES32 = solanaUsdc; + SOLANA_USDC_ADDRESS = _mapSolanaAddress(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 { + if (target != SOLANA_SPOKE_POOL_ADDRESS) { + revert InvalidRelayMessageTarget(target); + } + + bytes4 selector = bytes4(message[:4]); + if (selector == SpokePoolInterface.setEnableRoute.selector) { + 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); + } + + /** + * @notice Helper to map a Solana address to an Ethereum address representation. + * @dev The Ethereum address is derived from the Solana address by hashing it and then truncating 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 _mapSolanaAddress(bytes32 solanaAddress) internal pure returns (address) { + return address(uint160(uint256(keccak256(abi.encodePacked(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 + ); + } +} diff --git a/contracts/external/interfaces/CCTPInterfaces.sol b/contracts/external/interfaces/CCTPInterfaces.sol index 8431bbfdc..e932d943a 100644 --- a/contracts/external/interfaces/CCTPInterfaces.sol +++ b/contracts/external/interfaces/CCTPInterfaces.sol @@ -74,3 +74,24 @@ interface ITokenMinter { */ function burnLimitsPerMessage(address token) external view returns (uint256); } + +/** + * IMessageTransmitter in CCTP inherits IRelayer and IReceiver, but here we only import sendMessage from IRelayer: + * https://github.com/circlefin/evm-cctp-contracts/blob/377c9bd813fb86a42d900ae4003599d82aef635a/src/interfaces/IMessageTransmitter.sol#L25 + * https://github.com/circlefin/evm-cctp-contracts/blob/377c9bd813fb86a42d900ae4003599d82aef635a/src/interfaces/IRelayer.sol#L23-L35 + */ +interface IMessageTransmitter { + /** + * @notice Sends an outgoing message from the source domain. + * @dev Increment nonce, format the message, and emit `MessageSent` event with message information. + * @param destinationDomain Domain of destination chain + * @param recipient Address of message recipient on destination domain as bytes32 + * @param messageBody Raw bytes content of message + * @return nonce reserved by message + */ + function sendMessage( + uint32 destinationDomain, + bytes32 recipient, + bytes calldata messageBody + ) external returns (uint64); +} diff --git a/contracts/libraries/CircleCCTPAdapter.sol b/contracts/libraries/CircleCCTPAdapter.sol index 6403ed4c4..f1193fdb0 100644 --- a/contracts/libraries/CircleCCTPAdapter.sol +++ b/contracts/libraries/CircleCCTPAdapter.sol @@ -9,6 +9,7 @@ library CircleDomainIds { uint32 public constant Ethereum = 0; uint32 public constant Optimism = 2; uint32 public constant Arbitrum = 3; + uint32 public constant Solana = 5; uint32 public constant Base = 6; uint32 public constant Polygon = 7; // Use this value for placeholder purposes only for adapters that extend this adapter but haven't yet been @@ -87,6 +88,16 @@ abstract contract CircleCCTPAdapter { * @param amount Amount of USDC to transfer. */ function _transferUsdc(address to, uint256 amount) internal { + _transferUsdc(_addressToBytes32(to), amount); + } + + /** + * @notice Transfers USDC from the current domain to the given address on the new domain. + * @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. + * @param to Address to receive USDC on the new domain represented as bytes32. + * @param amount Amount of USDC to transfer. + */ + function _transferUsdc(bytes32 to, uint256 amount) internal { // Only approve the exact amount to be transferred usdcToken.safeIncreaseAllowance(address(cctpTokenMessenger), amount); // Submit the amount to be transferred to bridged via the TokenMessenger. @@ -94,10 +105,9 @@ abstract contract CircleCCTPAdapter { ITokenMinter cctpMinter = cctpTokenMessenger.localMinter(); uint256 burnLimit = cctpMinter.burnLimitsPerMessage(address(usdcToken)); uint256 remainingAmount = amount; - bytes32 recipient = _addressToBytes32(to); while (remainingAmount > 0) { uint256 partAmount = remainingAmount > burnLimit ? burnLimit : remainingAmount; - cctpTokenMessenger.depositForBurn(partAmount, recipientCircleDomainId, recipient, address(usdcToken)); + cctpTokenMessenger.depositForBurn(partAmount, recipientCircleDomainId, to, address(usdcToken)); remainingAmount -= partAmount; } } From ce0ceb4970ebd3de40f57fb82cc8d1089b73a204 Mon Sep 17 00:00:00 2001 From: Reinis Martinsons Date: Wed, 9 Oct 2024 15:04:21 +0000 Subject: [PATCH 2/5] fix: comments Signed-off-by: Reinis Martinsons --- contracts/chain-adapters/Solana_Adapter.sol | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/contracts/chain-adapters/Solana_Adapter.sol b/contracts/chain-adapters/Solana_Adapter.sol index 11223f42c..e08e0a181 100644 --- a/contracts/chain-adapters/Solana_Adapter.sol +++ b/contracts/chain-adapters/Solana_Adapter.sol @@ -26,26 +26,45 @@ contract Solana_Adapter is AdapterInterface, CircleCCTPAdapter { // 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, From d325c9226402590e285724b984bc942c8cd5e3f1 Mon Sep 17 00:00:00 2001 From: Reinis Martinsons Date: Thu, 10 Oct 2024 10:49:58 +0000 Subject: [PATCH 3/5] test: solana adapter Signed-off-by: Reinis Martinsons --- test/evm/hardhat/MerkleLib.utils.ts | 13 +- .../hardhat/chain-adapters/Solana_Adapter.ts | 140 ++++++++++++++++++ utils/abis.ts | 14 ++ utils/utils.ts | 10 ++ 4 files changed, 173 insertions(+), 4 deletions(-) create mode 100644 test/evm/hardhat/chain-adapters/Solana_Adapter.ts diff --git a/test/evm/hardhat/MerkleLib.utils.ts b/test/evm/hardhat/MerkleLib.utils.ts index 4886f1cb3..20a0e43d1 100644 --- a/test/evm/hardhat/MerkleLib.utils.ts +++ b/test/evm/hardhat/MerkleLib.utils.ts @@ -4,7 +4,7 @@ import { BigNumber, defaultAbiCoder, keccak256, - toBNWei, + toBNWeiWithDecimals, createRandomBytes32, Contract, } from "../../../utils/utils"; @@ -119,9 +119,14 @@ export async function constructSingleRelayerRefundTree(l2Token: Contract | Strin return { leaves, tree }; } -export async function constructSingleChainTree(token: string, scalingSize = 1, repaymentChain = repaymentChainId) { - const tokensSendToL2 = toBNWei(100 * scalingSize); - const realizedLpFees = toBNWei(10 * scalingSize); +export async function constructSingleChainTree( + token: string, + scalingSize = 1, + repaymentChain = repaymentChainId, + decimals = 18 +) { + const tokensSendToL2 = toBNWeiWithDecimals(100 * scalingSize, decimals); + const realizedLpFees = toBNWeiWithDecimals(10 * scalingSize, decimals); const leaves = buildPoolRebalanceLeaves( [repaymentChain], // repayment chain. In this test we only want to send one token to one chain. [[token]], // l1Token. We will only be sending 1 token to one chain. diff --git a/test/evm/hardhat/chain-adapters/Solana_Adapter.ts b/test/evm/hardhat/chain-adapters/Solana_Adapter.ts new file mode 100644 index 000000000..c934a4f54 --- /dev/null +++ b/test/evm/hardhat/chain-adapters/Solana_Adapter.ts @@ -0,0 +1,140 @@ +/* eslint-disable no-unused-expressions */ +import { + amountToLp, + refundProposalLiveness, + bondAmount, + mockRelayerRefundRoot, + mockSlowRelayRoot, +} from "./../constants"; +import { + ethers, + expect, + Contract, + createFakeFromABI, + FakeContract, + SignerWithAddress, + getContractFactory, + seedWallet, + randomAddress, + createRandomBytes32, + mapSolanaAddress, + toWeiWithDecimals, +} from "../../../../utils/utils"; +import { hubPoolFixture, enableTokensForLP } from "../fixtures/HubPool.Fixture"; +import { constructSingleChainTree } from "../MerkleLib.utils"; +import { + CCTPTokenMessengerInterface, + CCTPTokenMinterInterface, + CCTPMessageTransmitterInterface, +} from "../../../../utils/abis"; + +let hubPool: Contract, solanaAdapter: Contract, weth: Contract, usdc: Contract, timer: Contract, mockSpoke: Contract; +let owner: SignerWithAddress, dataWorker: SignerWithAddress, liquidityProvider: SignerWithAddress; +let cctpTokenMessenger: FakeContract, cctpMessageTransmitter: FakeContract, cctpTokenMinter: FakeContract; +let solanaSpokePoolBytes32: string, + solanaUsdcBytes32: string, + solanaSpokePoolUsdcVaultBytes32: string, + solanaSpokePoolAddress: string, + solanaUsdcAddress: string; + +const solanaChainId = 1234567890; // TODO: Decide how to represent Solana in Across as it does not have a chainId. +const solanaDomainId = 5; + +describe("Solana Chain Adapter", function () { + beforeEach(async function () { + [owner, dataWorker, liquidityProvider] = await ethers.getSigners(); + ({ weth, hubPool, mockSpoke, timer, usdc } = await hubPoolFixture()); + await seedWallet(dataWorker, [usdc], weth, amountToLp); + await seedWallet(liquidityProvider, [usdc], weth, amountToLp.mul(10)); + + await enableTokensForLP(owner, hubPool, weth, [weth, usdc]); + for (const token of [weth, usdc]) { + await token.connect(liquidityProvider).approve(hubPool.address, amountToLp); + await hubPool.connect(liquidityProvider).addLiquidity(token.address, amountToLp); + await token.connect(dataWorker).approve(hubPool.address, bondAmount.mul(10)); + } + + cctpTokenMessenger = await createFakeFromABI(CCTPTokenMessengerInterface); + cctpMessageTransmitter = await createFakeFromABI(CCTPMessageTransmitterInterface); + cctpTokenMinter = await createFakeFromABI(CCTPTokenMinterInterface); + cctpTokenMessenger.localMinter.returns(cctpTokenMinter.address); + cctpTokenMinter.burnLimitsPerMessage.returns(toWeiWithDecimals("1000000", 6)); + + solanaSpokePoolBytes32 = createRandomBytes32(); + solanaUsdcBytes32 = createRandomBytes32(); + solanaSpokePoolUsdcVaultBytes32 = createRandomBytes32(); + + solanaSpokePoolAddress = mapSolanaAddress(solanaSpokePoolBytes32); + solanaUsdcAddress = mapSolanaAddress(solanaUsdcBytes32); + + solanaAdapter = await ( + await getContractFactory("Solana_Adapter", owner) + ).deploy( + usdc.address, + cctpTokenMessenger.address, + cctpMessageTransmitter.address, + solanaSpokePoolBytes32, + solanaUsdcBytes32, + solanaSpokePoolUsdcVaultBytes32 + ); + + await hubPool.setCrossChainContracts(solanaChainId, solanaAdapter.address, solanaSpokePoolAddress); + await hubPool.setPoolRebalanceRoute(solanaChainId, usdc.address, solanaUsdcAddress); + }); + + it("relayMessage calls spoke pool functions", async function () { + const newAdmin = randomAddress(); + const functionCallData = mockSpoke.interface.encodeFunctionData("setCrossDomainAdmin", [newAdmin]); + expect(await hubPool.relaySpokePoolAdminFunction(solanaChainId, functionCallData)) + .to.emit(solanaAdapter.attach(hubPool.address), "MessageRelayed") + .withArgs(solanaSpokePoolAddress, functionCallData); + expect(cctpMessageTransmitter.sendMessage).to.have.been.calledWith( + solanaDomainId, + solanaSpokePoolBytes32, + functionCallData + ); + }); + + it("Correctly calls the CCTP bridge adapter when attempting to bridge USDC", async function () { + // Create an action that will send an L1->L2 tokens transfer and bundle. For this, create a relayer repayment bundle + // and check that at it's finalization the L2 bridge contracts are called as expected. + const { leaves, tree, tokensSendToL2 } = await constructSingleChainTree(usdc.address, 1, solanaChainId, 6); + await hubPool + .connect(dataWorker) + .proposeRootBundle([3117], 1, tree.getHexRoot(), mockRelayerRefundRoot, mockSlowRelayRoot); + await timer.setCurrentTime(Number(await timer.getCurrentTime()) + refundProposalLiveness + 1); + await hubPool.connect(dataWorker).executeRootBundle(...Object.values(leaves[0]), tree.getHexProof(leaves[0])); + + // Adapter should have approved CCTP TokenMessenger to spend its ERC20, but the fake instance does not pull them. + expect(await usdc.allowance(hubPool.address, cctpTokenMessenger.address)).to.equal(tokensSendToL2); + + // The correct functions should have been called on the CCTP TokenMessenger contract + expect(cctpTokenMessenger.depositForBurn).to.have.been.calledOnce; + expect(cctpTokenMessenger.depositForBurn).to.have.been.calledWith( + ethers.BigNumber.from(tokensSendToL2), + solanaDomainId, + solanaSpokePoolUsdcVaultBytes32, + usdc.address + ); + }); + + it("Correctly translates setEnableRoute calls to the spoke pool", async function () { + // Enable deposits for USDC on Solana. + const destinationChainId = 1; + const depositsEnabled = true; + await hubPool.setDepositRoute(solanaChainId, destinationChainId, solanaUsdcAddress, depositsEnabled); + + // Solana spoke pool expects to receive full bytes32 token address and uint64 for chainId. + const solanaInterface = new ethers.utils.Interface(["function setEnableRoute(bytes32, uint64, bool)"]); + const solanaMessage = solanaInterface.encodeFunctionData("setEnableRoute", [ + solanaUsdcBytes32, + destinationChainId, + depositsEnabled, + ]); + expect(cctpMessageTransmitter.sendMessage).to.have.been.calledWith( + solanaDomainId, + solanaSpokePoolBytes32, + solanaMessage + ); + }); +}); diff --git a/utils/abis.ts b/utils/abis.ts index 7a52b6f1c..f33c69027 100644 --- a/utils/abis.ts +++ b/utils/abis.ts @@ -51,3 +51,17 @@ export const CCTPTokenMinterInterface = [ type: "function", }, ]; + +export const CCTPMessageTransmitterInterface = [ + { + inputs: [ + { internalType: "uint32", name: "destinationDomain", type: "uint32" }, + { internalType: "bytes32", name: "recipient", type: "bytes32" }, + { internalType: "bytes", name: "messageBody", type: "bytes" }, + ], + name: "sendMessage", + outputs: [{ internalType: "uint64", name: "", type: "uint64" }], + stateMutability: "nonpayable", + type: "function", + }, +]; diff --git a/utils/utils.ts b/utils/utils.ts index 215580421..beedcb6e4 100644 --- a/utils/utils.ts +++ b/utils/utils.ts @@ -166,6 +166,16 @@ function avmL1ToL2Alias(l1Address: string) { return ethers.utils.hexlify(l2AddressAsNumber.mod(mask)); } +export function mapSolanaAddress(bytes32Address: string): string { + if (!ethers.utils.isHexString(bytes32Address, 32)) { + throw new Error("Invalid bytes32 address"); + } + + const hash = ethers.utils.keccak256(bytes32Address); + const uint160Address = ethers.BigNumber.from(hash).mask(160); + return ethers.utils.hexZeroPad(ethers.utils.hexlify(uint160Address), 20); +} + const { defaultAbiCoder, keccak256 } = ethers.utils; export { avmL1ToL2Alias, expect, Contract, ethers, BigNumber, defaultAbiCoder, keccak256, FakeContract, Signer }; From fd0d0563e9b2c300f760b105ae51aa5662208711 Mon Sep 17 00:00:00 2001 From: Reinis Martinsons <77973553+Reinis-FRP@users.noreply.github.com> Date: Fri, 11 Oct 2024 09:23:49 +0300 Subject: [PATCH 4/5] Update contracts/chain-adapters/Solana_Adapter.sol Co-authored-by: Chris Maree --- contracts/chain-adapters/Solana_Adapter.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/chain-adapters/Solana_Adapter.sol b/contracts/chain-adapters/Solana_Adapter.sol index e08e0a181..90da3fb6f 100644 --- a/contracts/chain-adapters/Solana_Adapter.sol +++ b/contracts/chain-adapters/Solana_Adapter.sol @@ -9,7 +9,7 @@ import { CircleCCTPAdapter, CircleDomainIds } from "../libraries/CircleCCTPAdapt import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /** - * @notice Contract containing logic to send messages from L1 to Solana. + * @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 From 3a8a728103975032dbfee18a5ac70ad664f02c7e Mon Sep 17 00:00:00 2001 From: Reinis Martinsons Date: Tue, 15 Oct 2024 07:46:59 +0000 Subject: [PATCH 5/5] fix: do not hash bytes32 svm address Signed-off-by: Reinis Martinsons --- contracts/chain-adapters/Solana_Adapter.sol | 14 +++++++------- test/evm/hardhat/chain-adapters/Solana_Adapter.ts | 6 +++--- utils/utils.ts | 5 ++--- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/contracts/chain-adapters/Solana_Adapter.sol b/contracts/chain-adapters/Solana_Adapter.sol index 90da3fb6f..8708889e7 100644 --- a/contracts/chain-adapters/Solana_Adapter.sol +++ b/contracts/chain-adapters/Solana_Adapter.sol @@ -84,10 +84,10 @@ contract Solana_Adapter is AdapterInterface, CircleCCTPAdapter { cctpMessageTransmitter = _cctpMessageTransmitter; SOLANA_SPOKE_POOL_BYTES32 = solanaSpokePool; - SOLANA_SPOKE_POOL_ADDRESS = _mapSolanaAddress(solanaSpokePool); + SOLANA_SPOKE_POOL_ADDRESS = _trimSolanaAddress(solanaSpokePool); SOLANA_USDC_BYTES32 = solanaUsdc; - SOLANA_USDC_ADDRESS = _mapSolanaAddress(solanaUsdc); + SOLANA_USDC_ADDRESS = _trimSolanaAddress(solanaUsdc); SOLANA_SPOKE_POOL_USDC_VAULT = solanaSpokePoolUsdcVault; } @@ -153,14 +153,14 @@ contract Solana_Adapter is AdapterInterface, CircleCCTPAdapter { /** * @notice Helper to map a Solana address to an Ethereum address representation. - * @dev The Ethereum address is derived from the Solana address by hashing it and then truncating 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. + * @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 _mapSolanaAddress(bytes32 solanaAddress) internal pure returns (address) { - return address(uint160(uint256(keccak256(abi.encodePacked(solanaAddress))))); + function _trimSolanaAddress(bytes32 solanaAddress) internal pure returns (address) { + return address(uint160(uint256(solanaAddress))); } /** diff --git a/test/evm/hardhat/chain-adapters/Solana_Adapter.ts b/test/evm/hardhat/chain-adapters/Solana_Adapter.ts index c934a4f54..a86a8fda8 100644 --- a/test/evm/hardhat/chain-adapters/Solana_Adapter.ts +++ b/test/evm/hardhat/chain-adapters/Solana_Adapter.ts @@ -17,7 +17,7 @@ import { seedWallet, randomAddress, createRandomBytes32, - mapSolanaAddress, + trimSolanaAddress, toWeiWithDecimals, } from "../../../../utils/utils"; import { hubPoolFixture, enableTokensForLP } from "../fixtures/HubPool.Fixture"; @@ -64,8 +64,8 @@ describe("Solana Chain Adapter", function () { solanaUsdcBytes32 = createRandomBytes32(); solanaSpokePoolUsdcVaultBytes32 = createRandomBytes32(); - solanaSpokePoolAddress = mapSolanaAddress(solanaSpokePoolBytes32); - solanaUsdcAddress = mapSolanaAddress(solanaUsdcBytes32); + solanaSpokePoolAddress = trimSolanaAddress(solanaSpokePoolBytes32); + solanaUsdcAddress = trimSolanaAddress(solanaUsdcBytes32); solanaAdapter = await ( await getContractFactory("Solana_Adapter", owner) diff --git a/utils/utils.ts b/utils/utils.ts index beedcb6e4..f526a29c4 100644 --- a/utils/utils.ts +++ b/utils/utils.ts @@ -166,13 +166,12 @@ function avmL1ToL2Alias(l1Address: string) { return ethers.utils.hexlify(l2AddressAsNumber.mod(mask)); } -export function mapSolanaAddress(bytes32Address: string): string { +export function trimSolanaAddress(bytes32Address: string): string { if (!ethers.utils.isHexString(bytes32Address, 32)) { throw new Error("Invalid bytes32 address"); } - const hash = ethers.utils.keccak256(bytes32Address); - const uint160Address = ethers.BigNumber.from(hash).mask(160); + const uint160Address = ethers.BigNumber.from(bytes32Address).mask(160); return ethers.utils.hexZeroPad(ethers.utils.hexlify(uint160Address), 20); }