Skip to content

Commit

Permalink
MultiOCR3 - integration to off-ramp & optimizations (#976)
Browse files Browse the repository at this point in the history
## Motivation
Sync MultiOCR3Base with CCIP Capability Config contract, and integrate
to the MultiOffRamp. With this integration, the multi-offramp still has
~2.5KB of remaining contract size

## Solution
* Refactor OCR2 to OCR3 for the MultiOffRamp, using the multi-plugin
OCR3 base (added `transmitExec` function)
* Remove signers.length == transmitters.length check in MultiOCR3 - this
is no longer a requirement - see [CCIP Config
Contract](https://github.com/smartcontractkit/ccip/pull/858/files/6de04055d2f456fe2ec62fb59d5942f8f0d426d8#diff-289f4b98c303dffb6740c8fd2ab6977b0fd4694604c788bb736963b30ea4d9edR359)
* Size optimizations:
* Create `_assignOracleRoles` and `_clearOracleRoles` helpers to re-use
transmitter and signer update flows - saves about 0.39KB space
* Optimize `InvalidConfig` error types to use enum error codes instead
of strings. Using strings is expensive, using enums is cheaper than
defining a custom error for each error
* Small gas golfing / removal of unnecessary variables in the
`setOCR3Configs`
* Fix incorrect `ConfigSet` event emission when assigning signers to an
OCR config with sigs disabled
  • Loading branch information
elatoskinas authored Jun 10, 2024
1 parent 82c3299 commit 542f51d
Show file tree
Hide file tree
Showing 12 changed files with 678 additions and 503 deletions.
5 changes: 5 additions & 0 deletions contracts/.changeset/shiny-rivers-know.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@chainlink/contracts-ccip": minor
---

#updated ocr3 integration to multi-offramp and ocr3 optimizations
197 changes: 101 additions & 96 deletions contracts/gas-snapshots/ccip.gas-snapshot

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions contracts/src/v0.8/ccip/libraries/Internal.sol
Original file line number Diff line number Diff line change
Expand Up @@ -181,4 +181,10 @@ library Internal {
SUCCESS,
FAILURE
}

/// @notice CCIP OCR plugin type, used to separate execution & commit transmissions and configs
enum OCRPluginType {
Commit,
Execution
}
}
83 changes: 49 additions & 34 deletions contracts/src/v0.8/ccip/ocr/MultiOCR3Base.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,15 @@ abstract contract MultiOCR3Base is ITypeAndVersion, OwnerIsCreator {
/// use latestConfigDigestAndEpoch with scanLogs set to false.
event Transmitted(uint8 indexed ocrPluginType, bytes32 configDigest, uint64 sequenceNumber);

error InvalidConfig(string message);
enum InvalidConfigErrorType {
F_MUST_BE_POSITIVE,
TOO_MANY_TRANSMITTERS,
TOO_MANY_SIGNERS,
F_TOO_HIGH,
REPEATED_ORACLE_ADDRESS
}

error InvalidConfig(InvalidConfigErrorType errorType);
error WrongMessageLength(uint256 expected, uint256 actual);
error ConfigDigestMismatch(bytes32 expected, bytes32 actual);
error ForkedChain(uint256 expected, uint256 actual);
Expand Down Expand Up @@ -115,6 +123,8 @@ abstract contract MultiOCR3Base is ITypeAndVersion, OwnerIsCreator {
}

/// @notice sets offchain reporting protocol configuration incl. participating oracles
/// NOTE: The OCR3 config must be sanity-checked against the home-chain registry configuration, to ensure
/// home-chain and remote-chain parity!
/// @param ocrConfigArgs OCR config update args
function setOCR3Configs(OCRConfigArgs[] memory ocrConfigArgs) external onlyOwner {
for (uint256 i; i < ocrConfigArgs.length; ++i) {
Expand All @@ -125,7 +135,7 @@ abstract contract MultiOCR3Base is ITypeAndVersion, OwnerIsCreator {
/// @notice sets offchain reporting protocol configuration incl. participating oracles for a single OCR plugin type
/// @param ocrConfigArgs OCR config update args
function _setOCR3Config(OCRConfigArgs memory ocrConfigArgs) internal {
if (ocrConfigArgs.F == 0) revert InvalidConfig("F must be positive");
if (ocrConfigArgs.F == 0) revert InvalidConfig(InvalidConfigErrorType.F_MUST_BE_POSITIVE);

uint8 ocrPluginType = ocrConfigArgs.ocrPluginType;
OCRConfig storage ocrConfig = s_ocrConfigs[ocrPluginType];
Expand All @@ -146,55 +156,60 @@ abstract contract MultiOCR3Base is ITypeAndVersion, OwnerIsCreator {
// Transmitters are expected to never exceed 255 (since this is bounded by MAX_NUM_ORACLES)
uint8 newTransmittersLength = uint8(transmitters.length);

if (newTransmittersLength > MAX_NUM_ORACLES) revert InvalidConfig("too many transmitters");
if (newTransmittersLength > MAX_NUM_ORACLES) revert InvalidConfig(InvalidConfigErrorType.TOO_MANY_TRANSMITTERS);

address[] memory oldTransmitters = ocrConfig.transmitters;
address[] memory oldSigners = ocrConfig.signers;
bool isSignatureVerificationEnabled = ocrConfigArgs.isSignatureVerificationEnabled;
for (uint256 i = 0; i < oldTransmitters.length; ++i) {
delete s_oracles[ocrPluginType][oldTransmitters[i]];
_clearOracleRoles(ocrPluginType, ocrConfig.transmitters);

// NOTE: oldSigners.length == oldTransmitters.length
if (isSignatureVerificationEnabled) {
delete s_oracles[ocrPluginType][oldSigners[i]];
}
}
if (ocrConfigArgs.isSignatureVerificationEnabled) {
_clearOracleRoles(ocrPluginType, ocrConfig.signers);

if (isSignatureVerificationEnabled) {
ocrConfig.signers = ocrConfigArgs.signers;
address[] memory signers = ocrConfigArgs.signers;
ocrConfig.signers = signers;

if (signers.length != newTransmittersLength) revert InvalidConfig("oracle addresses out of registration");
if (signers.length <= 3 * ocrConfigArgs.F) revert InvalidConfig("faulty-oracle F too high");
uint8 signersLength = uint8(signers.length);
configInfo.n = signersLength;

for (uint8 i = 0; i < newTransmittersLength; ++i) {
// add new signer/transmitter addresses
address signer = signers[i];
if (s_oracles[ocrPluginType][signer].role != Role.Unset) revert InvalidConfig("repeated signer address");
if (signer == address(0)) revert OracleCannotBeZeroAddress();
s_oracles[ocrPluginType][signer] = Oracle(uint8(i), Role.Signer);
}
}
if (signersLength > MAX_NUM_ORACLES) revert InvalidConfig(InvalidConfigErrorType.TOO_MANY_SIGNERS);
if (signersLength <= 3 * ocrConfigArgs.F) revert InvalidConfig(InvalidConfigErrorType.F_TOO_HIGH);

for (uint8 i = 0; i < newTransmittersLength; ++i) {
address transmitter = transmitters[i];
if (s_oracles[ocrPluginType][transmitter].role != Role.Unset) {
revert InvalidConfig("repeated transmitter address");
}
if (transmitter == address(0)) revert OracleCannotBeZeroAddress();
s_oracles[ocrPluginType][transmitter] = Oracle(uint8(i), Role.Transmitter);
_assignOracleRoles(ocrPluginType, signers, Role.Signer);
}

_assignOracleRoles(ocrPluginType, transmitters, Role.Transmitter);

ocrConfig.transmitters = transmitters;
configInfo.F = ocrConfigArgs.F;
configInfo.configDigest = ocrConfigArgs.configDigest;
configInfo.n = newTransmittersLength;

emit ConfigSet(
ocrPluginType, ocrConfigArgs.configDigest, ocrConfigArgs.signers, ocrConfigArgs.transmitters, ocrConfigArgs.F
ocrPluginType, ocrConfigArgs.configDigest, ocrConfig.signers, ocrConfigArgs.transmitters, ocrConfigArgs.F
);
}

/// @notice Clears oracle roles for the provided oracle addresses
/// @param ocrPluginType OCR plugin type to clear roles for
/// @param oracleAddresses Oracle addresses to clear roles for
function _clearOracleRoles(uint8 ocrPluginType, address[] memory oracleAddresses) internal {
for (uint256 i = 0; i < oracleAddresses.length; ++i) {
delete s_oracles[ocrPluginType][oracleAddresses[i]];
}
}

/// @notice Assigns oracles roles for the provided oracle addresses with uniqueness verification
/// @param ocrPluginType OCR plugin type to assign roles for
/// @param oracleAddresses Oracle addresses to assign roles to
/// @param role Role to assign
function _assignOracleRoles(uint8 ocrPluginType, address[] memory oracleAddresses, Role role) internal {
for (uint8 i = 0; i < oracleAddresses.length; ++i) {
address oracle = oracleAddresses[i];
if (s_oracles[ocrPluginType][oracle].role != Role.Unset) {
revert InvalidConfig(InvalidConfigErrorType.REPEATED_ORACLE_ADDRESS);
}
if (oracle == address(0)) revert OracleCannotBeZeroAddress();
s_oracles[ocrPluginType][oracle] = Oracle(i, role);
}
}

/// @notice _transmit is called to post a new report to the contract.
/// The function should be called after the per-DON reporting logic is completed.
/// @param ocrPluginType OCR plugin type to transmit report for
Expand Down
58 changes: 28 additions & 30 deletions contracts/src/v0.8/ccip/offRamp/EVM2EVMMultiOffRamp.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {EnumerableMapAddresses} from "../../shared/enumerable/EnumerableMapAddre
import {Client} from "../libraries/Client.sol";
import {Internal} from "../libraries/Internal.sol";
import {Pool} from "../libraries/Pool.sol";
import {OCR2BaseNoChecks} from "../ocr/OCR2BaseNoChecks.sol";
import {MultiOCR3Base} from "../ocr/MultiOCR3Base.sol";

import {IERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol";
import {ERC165Checker} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/introspection/ERC165Checker.sol";
Expand All @@ -25,10 +25,11 @@ import {ERC165Checker} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts
/// in an OffRamp in a single transaction.
/// @dev The EVM2EVMOnRamp, CommitStore and EVM2EVMOffRamp form an xchain upgradeable unit. Any change to one of them
/// results an onchain upgrade of all 3.
/// @dev OCR2BaseNoChecks is used to save gas, signatures are not required as the offramp can only execute
/// messages which are committed in the commitStore. We still make use of OCR2 as an executor whitelist
/// and turn-taking mechanism.
contract EVM2EVMMultiOffRamp is IAny2EVMMultiOffRamp, ITypeAndVersion, OCR2BaseNoChecks {
/// @dev MultiOCR3Base is used to store multiple OCR configs for both the OffRamp and the CommitStore.
/// The execution plugin type has to be configured without signature verification, and the commit
/// plugin type with verification.
// TODO: merge with MultiCommitStore
contract EVM2EVMMultiOffRamp is IAny2EVMMultiOffRamp, ITypeAndVersion, MultiOCR3Base {
using ERC165Checker for address;
using EnumerableMapAddresses for EnumerableMapAddresses.AddressToAddressMap;

Expand Down Expand Up @@ -133,10 +134,6 @@ contract EVM2EVMMultiOffRamp is IAny2EVMMultiOffRamp, ITypeAndVersion, OCR2BaseN
// DYNAMIC CONFIG
DynamicConfig internal s_dynamicConfig;

// TODO: evaluate whether this should be pulled in (since this can be inferred from SourceChainSelectorAdded events instead)
/// @notice all source chains available in s_sourceChainConfigs
// uint64[] internal s_sourceChainSelectors;

/// @notice SourceConfig per chain
/// (forms lane configurations from sourceChainSelector => StaticConfig.chainSelector)
mapping(uint64 sourceChainSelector => SourceChainConfig) internal s_sourceChainConfigs;
Expand All @@ -153,7 +150,7 @@ contract EVM2EVMMultiOffRamp is IAny2EVMMultiOffRamp, ITypeAndVersion, OCR2BaseN
mapping(uint64 sourceChainSelector => mapping(uint64 seqNum => uint256 executionStateBitmap)) internal
s_executionStates;

constructor(StaticConfig memory staticConfig, SourceChainConfigArgs[] memory sourceChainConfigs) OCR2BaseNoChecks() {
constructor(StaticConfig memory staticConfig, SourceChainConfigArgs[] memory sourceChainConfigs) MultiOCR3Base() {
if (staticConfig.commitStore == address(0)) revert ZeroAddressNotAllowed();

i_commitStore = staticConfig.commitStore;
Expand Down Expand Up @@ -252,8 +249,9 @@ contract EVM2EVMMultiOffRamp is IAny2EVMMultiOffRamp, ITypeAndVersion, OCR2BaseN
Internal.ExecutionReportSingleChain[] memory reports,
uint256[][] memory gasLimitOverrides
) external {
// We do this here because the other _execute path is already covered OCR2BaseXXX.
if (i_chainID != block.chainid) revert OCR2BaseNoChecks.ForkedChain(i_chainID, uint64(block.chainid));
// We do this here because the other _execute path is already covered by MultiOCR3Base.
// TODO: contract size golfing - split to internal function
if (i_chainID != block.chainid) revert MultiOCR3Base.ForkedChain(i_chainID, uint64(block.chainid));

uint256 numReports = reports.length;
if (numReports != gasLimitOverrides.length) revert ManualExecutionGasLimitMismatch();
Expand All @@ -277,12 +275,21 @@ contract EVM2EVMMultiOffRamp is IAny2EVMMultiOffRamp, ITypeAndVersion, OCR2BaseN
_batchExecute(reports, gasLimitOverrides);
}

/// @notice Entrypoint for execution, called by the OCR network
/// @dev Expects an encoded ExecutionReport
function _report(bytes calldata report) internal override {
Internal.ExecutionReportSingleChain[] memory reports = abi.decode(report, (Internal.ExecutionReportSingleChain[]));
/// @notice Transmit function for execution reports. The function takes no signatures,
/// and expects the exec plugin type to be configured with no signatures.
/// @param report serialized execution report
function transmitExec(bytes32[3] calldata reportContext, bytes calldata report) external {
_reportExec(report);

// TODO: gas / contract size saving from CONSTANT?
bytes32[] memory emptySigs = new bytes32[](0);
_transmit(uint8(Internal.OCRPluginType.Execution), reportContext, report, emptySigs, emptySigs, bytes32(""));
}

_batchExecute(reports, new uint256[][](0));
/// @notice Reporting function for the execution plugin
/// @param report encoded ExecutionReport
function _reportExec(bytes calldata report) internal {
_batchExecute(abi.decode(report, (Internal.ExecutionReportSingleChain[])), new uint256[][](0));
}

/// @notice Batch executes a set of reports, each report matching one single source chain
Expand Down Expand Up @@ -599,12 +606,6 @@ contract EVM2EVMMultiOffRamp is IAny2EVMMultiOffRamp, ITypeAndVersion, OCR2BaseN
return s_sourceChainConfigs[sourceChainSelector];
}

/// @notice Returns all configured source chain selectors
/// @return sourceChainSelectors source chain selectors
// function getSourceChainSelectors() external view returns (uint64[] memory) {
// return s_sourceChainSelectors;
// }

/// @notice Updates source configs
/// @param sourceChainConfigUpdates Source chain configs
function applySourceChainConfigUpdates(SourceChainConfigArgs[] memory sourceChainConfigUpdates) external onlyOwner {
Expand Down Expand Up @@ -645,7 +646,6 @@ contract EVM2EVMMultiOffRamp is IAny2EVMMultiOffRamp, ITypeAndVersion, OCR2BaseN
currentConfig.onRamp = sourceConfigUpdate.onRamp;
currentConfig.prevOffRamp = sourceConfigUpdate.prevOffRamp;

// s_sourceChainSelectors.push(sourceChainSelector);
emit SourceChainSelectorAdded(sourceChainSelector);
} else if (
currentConfig.onRamp != sourceConfigUpdate.onRamp || currentConfig.prevOffRamp != sourceConfigUpdate.prevOffRamp
Expand All @@ -659,17 +659,15 @@ contract EVM2EVMMultiOffRamp is IAny2EVMMultiOffRamp, ITypeAndVersion, OCR2BaseN
}
}

// TODO: _beforeSetConfig is no longer used in OCR3 - replace this with an external onlyOwner function
/// @notice Sets the dynamic config. This function is called during `setOCR2Config` flow
function _beforeSetConfig(bytes memory onchainConfig) internal override {
DynamicConfig memory dynamicConfig = abi.decode(onchainConfig, (DynamicConfig));

/// @notice Sets the dynamic config.
function setDynamicConfig(DynamicConfig memory dynamicConfig) external onlyOwner {
if (dynamicConfig.router == address(0)) revert ZeroAddressNotAllowed();

s_dynamicConfig = dynamicConfig;

// TODO: contract size golfing - is StaticConfig needed in the event?
emit ConfigSet(
StaticConfig({commitStore: i_commitStore, chainSelector: i_chainSelector, rmnProxy: i_rmnProxy}), dynamicConfig
StaticConfig({chainSelector: i_chainSelector, rmnProxy: i_rmnProxy, commitStore: i_commitStore}), dynamicConfig
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ contract EVM2EVMMultiOffRampHelper is EVM2EVMMultiOffRamp, IgnoreContractSize {
return _trialExecute(message, offchainTokenData);
}

function report(bytes calldata executableReports) external {
_report(executableReports);
function reportExec(bytes calldata executableReports) external {
_reportExec(executableReports);
}

function execute(Internal.ExecutionReportSingleChain memory rep, uint256[] memory manualExecGasLimits) external {
Expand Down
Loading

0 comments on commit 542f51d

Please sign in to comment.