Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ff47adc
bridge contracts
voith Feb 6, 2025
2e2f8d9
linting
voith Feb 7, 2025
025990c
added unit tests
voith Feb 7, 2025
6621320
added audit readme
voith Feb 7, 2025
77db2ab
fixed file name
voith Feb 7, 2025
1912759
added a complete integration test
voith Feb 10, 2025
cc366a7
implemented the changes suggested as general informational issues
voith Feb 23, 2025
30e7bfc
fixed typos and followed style guide for constants
voith Feb 24, 2025
2f26d7d
followed style guide for ccipRouter
voith Feb 24, 2025
de6f602
[Cantina Finding #8] Removed receive and withdraw functions for Gover…
voith Feb 23, 2025
50bd75d
[cantina finding #6] added mapping for store destination receivers
voith Feb 23, 2025
99d8c2c
added updateDestinationReceiver and implemeted other suggested fixes
voith Feb 24, 2025
08e62c9
added chainselector to DestinationReceiverUpdated
voith Feb 24, 2025
b7aa955
[cantina finding #5 ]added missing sanity checks
voith Feb 23, 2025
94bdc88
[cantina finding #7] enhanced logs
voith Feb 23, 2025
fa81632
[cantina finding #9] added gasLimit to relayMessage
voith Feb 23, 2025
ce766b4
[cantina finding #11] do not revert inside _ccipReceive
voith Feb 23, 2025
f94983a
fixed natspec and renamed _error to failure
voith Feb 24, 2025
fce963a
[cantina finding #3] messages cannot be replayed and cn be paused by …
voith Feb 23, 2025
f7bb37b
do not revert when paused
voith Feb 24, 2025
917e940
added unpause
voith Feb 24, 2025
c92e80c
Merge pull request #168 from cryptexfinance/remove-eth-functions
voith Feb 28, 2025
87bc3c0
Merge pull request #167 from cryptexfinance/general-info
voith Feb 28, 2025
f990033
removed broadcast dir
voith Feb 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions contracts/base/CryptexBaseTreasury.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.7.5;

import "../BaseTreasury.sol";

/**
* @title CryptexBaseTreasury
* @author Cryptex.finance
* @notice This contract will hold assets on the base network.
*/
contract CryptexBaseTreasury is BaseTreasury {
/**
* @notice Constructor
* @param _owner the owner of the contract
**/
constructor(address _owner) BaseTreasury(_owner) {}

/// @notice renounceOwnership has been disabled so that the contract is never left without a owner
/// @inheritdoc Proprietor
function renounceOwnership() public override onlyOwner {
revert("CryptexBaseTreasury::renounceOwnership: function disabled");
}
}
68 changes: 68 additions & 0 deletions contracts/ccip/AUDIT_README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Audit of Bridge contracts using CCIP

These contracts are designed to relay governance proposals from the [Governance contract](../governance/GovernorBeta.sol)
to other networks for execution. The bridge does not transfer funds but solely relays and executes governance messages on the destination chain.

### Use Cases:
- Modify parameters of contract owned by Governance on the destination chain
- Move funds from the [Treasury](../base/CryptexBaseTreasury.sol).

### contracts that need to be audited
- [GovernanceCCIPRelay.sol](./GovernanceCCIPRelay.sol)
- [GovernanceCCIPReceiver.sol](./GovernanceCCIPReceiver.sol)
- [CryptexBaseTreasury](../base/CryptexBaseTreasury.sol)

Note: For [CryptexBaseTreasury](../base/CryptexBaseTreasury.sol) assume that the
[BaseTreasury](../BaseTreasury.sol) is already audited. Only review the onlyOwner modifier.

### Test
- Tests for these contracts can be found in the [test directory](../../test/ccip/)
- [GovernanceCCIPIntegrationTest.t.sol](../../test/ccip/GovernanceCCIPIntegrationTest.t.sol) is the integration test for these contracts.

### Architecture
┌──────────────────────────┐
│ Governance Contract │
│ (GovernorBeta.sol) │
└──────────┬───────────────┘
┌──────────────────────────┐
│ Timelock │
│ (Timelock.sol) │
└──────────┬───────────────┘
┌──────────────────────────┐
│ GovernanceCCIPRelay.sol │
│ (Ethereum Mainnet) │
└──────────┬───────────────┘
┌──────────────────────────┐
│ CCIP Router (Mainnet)│
└──────────┬───────────────┘
┌──────────────────────────┐
│ CCIP Router (Dest) │
└──────────┬───────────────┘
┌──────────────────────────┐
│ GovernanceCCIPReceiver │
│ (Destination Chain) │
└──────────┬───────────────┘
┌──────────────────────────┐
│ Contract on Dest Chain │
│ (Executes Proposal) │
└──────────────────────────┘
### Workflow
1. Governance Contract creates a proposal that requires execution on another chain.
2. The Timelock executes the proposal and sends it to GovernanceCCIPRelay.sol on Ethereum.
3. GovernanceCCIPRelay.sol sends the message to the CCIP Router on Ethereum Mainnet.
4. The CCIP Router relays the message to the CCIP Router on the destination chain.
5. GovernanceCCIPReceiver.sol receives the message and validates the sender.
6. If valid, GovernanceCCIPReceiver.sol executes the proposal on the destination chain.

93 changes: 93 additions & 0 deletions contracts/ccip/GovernanceCCIPReceiver.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import {CCIPReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/applications/CCIPReceiver.sol";
import {IGovernanceCCIPReceiver} from "./interfaces/IGovernanceCCIPReceiver.sol";
import {Ownable} from "@openzeppelin/contracts-v5/access/Ownable.sol";
import {Pausable} from "@openzeppelin/contracts-v5/utils/Pausable.sol";

/**
* @title GovernanceCCIPReceiver
* @dev A contract for receiving and executing governance proposals from Ethereum Mainnet via CCIP.
* This contract processes cross-chain messages and executes them on the destination chain.
*/
contract GovernanceCCIPReceiver is
IGovernanceCCIPReceiver,
CCIPReceiver,
Ownable,
Pausable
{
/// @inheritdoc IGovernanceCCIPReceiver
address public immutable MAINNET_SENDER;

/// @inheritdoc IGovernanceCCIPReceiver
uint64 public constant MAINNET_CHAIN_SELECTOR = 5009297550715157269;
mapping(bytes32 => bool) public processedMessages;

/// @dev Constructor to initialize the GovernanceCCIPReceiver contract.
/// @param _router The address of the CCIP router contract on the destination chain.
/// @param _sender The address of the mainnet sender.
constructor(
address _router,
address _sender,
address _owner
) CCIPReceiver(_router) Ownable(_owner) {
require(_sender != address(0), AddressCannotBeZero());
MAINNET_SENDER = _sender;
}

/// @inheritdoc IGovernanceCCIPReceiver
function pause() external onlyOwner {
_pause();
}

/// @inheritdoc IGovernanceCCIPReceiver
function unpause() external onlyOwner {
_unpause();
}

/// @notice Handles incoming CCIP messages and executes the payload.
/// @dev This function processes cross-chain messages from Ethereum Mainnet.
/// @param message The CCIP message containing the target address and payload.
function _ccipReceive(Client.Any2EVMMessage memory message)
internal
override
{
bytes32 messageId = message.messageId;
// check if message has been processed before
require(!processedMessages[messageId], MessageAlreadyProcessed(messageId));
processedMessages[messageId] = true;

if (paused()) {
emit MessageIgnoredWhilePaused(messageId);
return;
}

// Validate chain selector
require(
message.sourceChainSelector == MAINNET_CHAIN_SELECTOR,
InvalidChainSelector()
);

// Validate sender
address messageSender = abi.decode(message.sender, (address));
require(messageSender == MAINNET_SENDER, Unauthorized(messageSender));

// Decode payload
(address target, bytes memory payload) = abi.decode(
message.data,
(address, bytes)
);

require(target != address(0), TargetAddressCannotBeZero());

// Execute payload
(bool success, bytes memory failure) = target.call(payload);
if (success) {
emit MessageExecutedSuccessfully(messageId, target, payload);
} else {
emit MessageExecutionFailed(messageId, target, payload, failure);
}
}
}
188 changes: 188 additions & 0 deletions contracts/ccip/GovernanceCCIPRelay.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import {IGovernanceCCIPRelay} from "./interfaces/IGovernanceCCIPRelay.sol";

/**
* @title GovernanceCCIPRelay
* @dev A contract for relaying governance proposals from Ethereum Mainnet to Destination Chain using CCIP.
* This contract allows the Timelock contract to send cross-chain messages to a receiver contract on the destination chain..
*/
contract GovernanceCCIPRelay is IGovernanceCCIPRelay {
/// @inheritdoc IGovernanceCCIPRelay
IRouterClient public immutable CCIP_ROUTER;

/// @inheritdoc IGovernanceCCIPRelay
address public immutable TIMELOCK;

/// @inheritdoc IGovernanceCCIPRelay
mapping(uint64 => address) public destinationReceivers;

uint64 private constant CCIP_MAINNET_CHAIN_SELECTOR = 5009297550715157269;
uint256 constant MIN_GAS_LIMIT = 50_000;
uint256 constant MAX_GAS_LIMIT = 10_000_000;

/// @dev Modifier to restrict access to the Timelock contract.
modifier onlyTimeLock() {
require(msg.sender == TIMELOCK, Unauthorized(msg.sender));
_;
}

/// @dev Constructor to initialize the GovernanceCCIPRelay contract.
/// @param _timelock The address of the Timelock contract.
/// @param _router The address of the CCIP router contract.
/// @param _destinationChainSelectors Array of chain selectors for the destination chains.
/// @param _destinationReceivers Array addresses of the receiver contracts on the destination chains.
constructor(
address _timelock,
address _router,
uint64[] memory _destinationChainSelectors,
address[] memory _destinationReceivers
) {
require(_timelock != address(0), AddressCannotBeZero());
require(_router != address(0), AddressCannotBeZero());
CCIP_ROUTER = IRouterClient(_router);
TIMELOCK = _timelock;
_addDestinationChains(_destinationChainSelectors, _destinationReceivers);
}

/// @inheritdoc IGovernanceCCIPRelay
function addDestinationChains(
uint64[] memory _destinationChainSelectors,
address[] memory _destinationReceivers
) external onlyTimeLock {
_addDestinationChains(_destinationChainSelectors, _destinationReceivers);
}

function _addDestinationChains(
uint64[] memory _destinationChainSelectors,
address[] memory _destinationReceivers
) private {
uint256 receiversLength = _destinationReceivers.length;
require(
_destinationChainSelectors.length == receiversLength,
ArrayLengthMismatch()
);

for (uint256 i = 0; i < receiversLength; ) {
uint64 _destinationChainSelector = _destinationChainSelectors[i];
address _destinationReceiver = _destinationReceivers[i];

require(
CCIP_ROUTER.isChainSupported(_destinationChainSelector),
DestinationChainNotSupported(_destinationChainSelector)
);

require(
_destinationChainSelector != CCIP_MAINNET_CHAIN_SELECTOR,
CannotUseMainnetChainSelector()
);

require(
_destinationReceiver != address(0),
ReceiverCannotBeZeroAddress()
);

require(
destinationReceivers[_destinationChainSelector] == address(0),
ChainSelectorAlreadyAssigned(_destinationChainSelector)
);

destinationReceivers[_destinationChainSelector] = _destinationReceiver;
emit DestinationChainAdded(
_destinationChainSelector,
_destinationReceiver
);

unchecked {
i++;
}
}
}

/// @inheritdoc IGovernanceCCIPRelay
function updateDestinationReceiver(
uint64 _destinationChainSelector,
address _destinationReceiver
) external onlyTimeLock {
address oldDestinationReceiver = destinationReceivers[
_destinationChainSelector
];

require(
oldDestinationReceiver != address(0),
DestinationChainIsNotAdded(_destinationChainSelector)
);
require(_destinationReceiver != address(0), ReceiverCannotBeZeroAddress());
require(
oldDestinationReceiver != _destinationReceiver,
ReceiverUnchanged()
);

destinationReceivers[_destinationChainSelector] = _destinationReceiver;
emit DestinationReceiverUpdated(
_destinationChainSelector,
oldDestinationReceiver,
_destinationReceiver
);
}

/// @inheritdoc IGovernanceCCIPRelay
function relayMessage(
uint64 destinationChainSelector,
uint256 gasLimit,
address target,
bytes calldata payload
) external payable onlyTimeLock returns (bytes32 messageId) {
require(target != address(0), AddressCannotBeZero());
require(payload.length != 0, PayloadCannotBeEmpty());
require(gasLimit >= MIN_GAS_LIMIT, GasLimitTooLow(gasLimit, MIN_GAS_LIMIT));
require(
gasLimit <= MAX_GAS_LIMIT,
GasLimitTooHigh(gasLimit, MAX_GAS_LIMIT)
);

address destinationReceiver = destinationReceivers[
destinationChainSelector
];
require(
destinationReceiver != address(0),
DestinationChainIsNotAdded(destinationChainSelector)
);
// Create the CCIP message
Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({
receiver: abi.encode(destinationReceiver),
data: abi.encode(target, payload),
tokenAmounts: new Client.EVMTokenAmount[](0),
extraArgs: Client._argsToBytes(
Client.EVMExtraArgsV2({
gasLimit: gasLimit,
allowOutOfOrderExecution: true
})
),
feeToken: address(0)
});

// Calculate the required fee
uint256 msgValue = msg.value;
uint256 fee = CCIP_ROUTER.getFee(destinationChainSelector, message);
require(msgValue >= fee, InsufficientFee(msg.value, fee));

// Send the message via CCIP
messageId = CCIP_ROUTER.ccipSend{value: fee}(
destinationChainSelector,
message
);

// Refund excess Ether to the sender
uint256 excess = msg.value - fee;
if (excess > 0) {
(bool success, ) = msg.sender.call{value: excess}("");
require(success, FailedToRefundEth());
}

emit MessageRelayed(messageId, target, payload);
}
}
Loading
Loading