Skip to content
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

Native Minting Transaction Verification #30

Merged
merged 15 commits into from
Jan 3, 2025
15 changes: 15 additions & 0 deletions audit/20241209-scroll-revceiver-upgradeable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# [NM-0217] `ScrollReceiverUpgradable` change

**File(s)**: [1ScrollReceiverETHUpgradeable.sol](https://github.com/etherfi-protocol/weETH-cross-chain/blob/fc48e31ec1cb51a006b8354da93b0bb56586f278/contracts/NativeMinting/ReceiverContracts/L1ScrollReceiverETHUpgradeable.sol)

### Summary

The purpose of this PR is to ensure that messages only orginated from the L2 sync pool contract.

---

### Findings

After reviewing the updated code, we don't see any clear risk on the changes that were implemented. The code seems to work as expected.

---
4 changes: 2 additions & 2 deletions foundry.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[profile.default]
src = "contracts"
evm_version = "shanghai"
evm_version = "paris"
out = "out"
libs = ["node_modules", "lib"]
test = "test"
Expand All @@ -9,7 +9,7 @@ fs_permissions = [{ access = "read-write", path = "./"}]
optimizer = true
optimizer_runs = 200
via_ir = true
solc_version = "0.8.24"
solc_version = "0.8.20"


# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
220 changes: 220 additions & 0 deletions scripts/ContractCodeChecker.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import {console} from "forge-std/console.sol";
import {console2} from "forge-std/console2.sol";


contract ContractCodeChecker {

event ByteMismatchSegment(
uint256 startIndex,
uint256 endIndex,
bytes aSegment,
bytes bSegment
);

function compareBytes(bytes memory a, bytes memory b) internal returns (bool) {
if (a.length != b.length) {
// Length mismatch, emit one big segment for the difference if that’s desirable
// or just return false. For clarity, we can just return false here.
return false;
}

uint256 len = a.length;
uint256 start = 0;
bool inMismatch = false;
bool anyMismatch = false;

for (uint256 i = 0; i < len; i++) {
bool mismatch = (a[i] != b[i]);
if (mismatch && !inMismatch) {
// Starting a new mismatch segment
start = i;
inMismatch = true;
} else if (!mismatch && inMismatch) {
// Ending the current mismatch segment at i-1
emitMismatchSegment(a, b, start, i - 1);
inMismatch = false;
anyMismatch = true;
}
}

// If we ended with a mismatch still open, close it out
if (inMismatch) {
emitMismatchSegment(a, b, start, len - 1);
anyMismatch = true;
}

// If no mismatch segments were found, everything matched
return !anyMismatch;
}

function emitMismatchSegment(
bytes memory a,
bytes memory b,
uint256 start,
uint256 end
) internal {
// endIndex is inclusive
uint256 segmentLength = end - start + 1;

bytes memory aSegment = new bytes(segmentLength);
bytes memory bSegment = new bytes(segmentLength);

for (uint256 i = 0; i < segmentLength; i++) {
aSegment[i] = a[start + i];
bSegment[i] = b[start + i];
}

string memory aHex = bytesToHexString(aSegment);
string memory bHex = bytesToHexString(bSegment);

console2.log("- Mismatch segment at index [%s, %s]", start, end);
console2.logString(string.concat(" - ", aHex));
console2.logString(string.concat(" - ", bHex));

emit ByteMismatchSegment(start, end, aSegment, bSegment);
}

function bytesToHexString(bytes memory data) internal pure returns (string memory) {
bytes memory alphabet = "0123456789abcdef";

// Every byte corresponds to two hex characters
bytes memory str = new bytes(2 + data.length * 2);
str[0] = '0';
str[1] = 'x';
for (uint256 i = 0; i < data.length; i++) {
str[2 + i * 2] = alphabet[uint8(data[i] >> 4)];
str[3 + i * 2] = alphabet[uint8(data[i] & 0x0f)];
}
return string(str);
}

// Compare the full bytecode of two deployed contracts, ensuring a perfect match.
function verifyFullMatch(bytes memory localBytecode, bytes memory onchainRuntimeBytecode) public {
console2.log("Verifying full bytecode match...");

if (compareBytes(localBytecode, onchainRuntimeBytecode)) {
console2.log("-> Full Bytecode Match: Success\n");
} else {
console2.log("-> Full Bytecode Match: Fail\n");
}
}

function verifyPartialMatch(bytes memory localBytecode, bytes memory onchainRuntimeBytecode) public {
console2.log("Verifying partial bytecode match...");

// Optionally check length first (not strictly necessary if doing a partial match)
if (localBytecode.length == 0 || onchainRuntimeBytecode.length == 0) {
revert("One of the bytecode arrays is empty, cannot verify.");
}

// Attempt to trim metadata from both local and on-chain bytecode
bytes memory trimmedLocal = trimMetadata(localBytecode);
bytes memory trimmedOnchain = trimMetadata(onchainRuntimeBytecode);

// If trimmed lengths differ significantly, it suggests structural differences in code
if (trimmedLocal.length != trimmedOnchain.length) {
console2.log("Post-trim length mismatch: potential code differences.");
}

// Compare trimmed arrays byte-by-byte
if (compareBytes(trimmedLocal, trimmedOnchain)) {
console2.log("-> Partial Bytecode Match: Success\n");
} else {
console2.log("-> Partial Bytecode Match: Fail\n");
}
}

function verifyLengthMatch(bytes memory localBytecode, bytes memory onchainRuntimeBytecode) public {
console2.log("Verifying length match...");

if (localBytecode.length == onchainRuntimeBytecode.length) {
console2.log("-> Length Match: Success");
} else {
console2.log("-> Length Match: Fail");
}
console2.log("Bytecode Length: ", localBytecode.length, "\n");
}

function verifyContractByteCodeMatchFromAddress(address deployedImpl, address localDeployed) public {
verifyLengthMatch(deployedImpl.code, localDeployed.code);
verifyPartialMatch(deployedImpl.code, localDeployed.code);
// verifyFullMatch(deployedImpl.code, localDeployed.code);
}

function verifyContractByteCodeMatchFromByteCode(bytes memory deployedImpl, bytes memory localDeployed) public {
verifyLengthMatch(deployedImpl, localDeployed);
verifyPartialMatch(deployedImpl, localDeployed);
// verifyFullMatch(deployedImpl, localDeployed);
}

// Known CBOR patterns for Solidity metadata:
// "a2 64 73 6f 6c 63" -> a2 (map with 2 pairs), 64 (4-char string), 's' 'o' 'l' 'c'
// "a2 64 69 70 66 73" -> a2 (map with 2 pairs), 64 (4-char string), 'i' 'p' 'f' 's'
bytes constant SOLC_PATTERN = hex"a264736f6c63"; // "a2 64 73 6f 6c 63"
bytes constant IPFS_PATTERN = hex"a26469706673"; // "a2 64 69 70 66 73"

function trimMetadata(bytes memory code) internal pure returns (bytes memory) {
uint256 length = code.length;
if (length < SOLC_PATTERN.length) {
// Bytecode too short to contain metadata
return code;
}

// Try to find a known pattern from the end.
// We'll look for either the "solc" pattern or the "ipfs" pattern.
int256 solcIndex = lastIndexOf(code, SOLC_PATTERN);
int256 ipfsIndex = lastIndexOf(code, IPFS_PATTERN);

// Determine which pattern was found later (nearer to the end).
int256 metadataIndex;
if (solcIndex >= 0 && ipfsIndex >= 0) {
metadataIndex = solcIndex > ipfsIndex ? solcIndex : ipfsIndex;
} else if (solcIndex >= 0) {
metadataIndex = solcIndex;
} else if (ipfsIndex >= 0) {
metadataIndex = ipfsIndex;
} else {
// No known pattern found, return code as is
return code;
}

console2.log("Original bytecode length: ", length);
console2.log("Trimmed metadata from bytecode at index: ", metadataIndex);

// metadataIndex is where metadata starts
bytes memory trimmed = new bytes(uint256(metadataIndex));
for (uint256 i = 0; i < uint256(metadataIndex); i++) {
trimmed[i] = code[i];
}
return trimmed;
}

// Helper function: Finds the last occurrence of `pattern` in `data`.
// Returns -1 if not found, otherwise returns the starting index.
function lastIndexOf(bytes memory data, bytes memory pattern) internal pure returns (int256) {
if (pattern.length == 0 || pattern.length > data.length) {
return -1;
}

// Start from the end of `data` and move backward
for (uint256 i = data.length - pattern.length; /* no condition */; i--) {
bool matchFound = true;
for (uint256 j = 0; j < pattern.length; j++) {
if (data[i + j] != pattern[j]) {
matchFound = false;
break;
}
}
if (matchFound) {
return int256(i);
}
if (i == 0) break; // Prevent underflow
}

return -1;
}

}
66 changes: 33 additions & 33 deletions scripts/NativeMintingDeployment/DeployConfigureL1.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,40 +19,40 @@ contract L1NativeMintingScript is Script, L2Constants, LayerZeroHelpers, GnosisH

// vm.startBroadcast(DEPLOYER_ADDRESS);

console.log("Deploying contracts on L1...");
// console.log("Deploying contracts on L1...");

address dummyTokenImpl = address(new DummyTokenUpgradeable{salt: keccak256("ScrollDummyTokenImpl")}(18));
address dummyTokenProxy = address(
new TransparentUpgradeableProxy{salt: keccak256("ScrollDummyToken")}(
dummyTokenImpl,
L1_TIMELOCK,
abi.encodeWithSelector(
DummyTokenUpgradeable.initialize.selector, "Scroll Dummy ETH", "scrollETH", DEPLOYER_ADDRESS
)
)
);
console.log("DummyToken deployed at: ", dummyTokenProxy);
require(dummyTokenProxy == SCROLL.L1_DUMMY_TOKEN, "Dummy Token address mismatch");
// address dummyTokenImpl = address(new DummyTokenUpgradeable{salt: keccak256("ScrollDummyTokenImpl")}(18));
// address dummyTokenProxy = address(
// new TransparentUpgradeableProxy{salt: keccak256("ScrollDummyToken")}(
// dummyTokenImpl,
// L1_TIMELOCK,
// abi.encodeWithSelector(
// DummyTokenUpgradeable.initialize.selector, "Scroll Dummy ETH", "scrollETH", DEPLOYER_ADDRESS
// )
// )
// );
// console.log("DummyToken deployed at: ", dummyTokenProxy);
// require(dummyTokenProxy == SCROLL.L1_DUMMY_TOKEN, "Dummy Token address mismatch");

DummyTokenUpgradeable dummyToken = DummyTokenUpgradeable(dummyTokenProxy);
dummyToken.grantRole(MINTER_ROLE, L1_SYNC_POOL);
dummyToken.grantRole(DEFAULT_ADMIN_ROLE, L1_CONTRACT_CONTROLLER);
dummyToken.renounceRole(DEFAULT_ADMIN_ROLE, DEPLOYER_ADDRESS);
// DummyTokenUpgradeable dummyToken = DummyTokenUpgradeable(dummyTokenProxy);
// dummyToken.grantRole(MINTER_ROLE, L1_SYNC_POOL);
// dummyToken.grantRole(DEFAULT_ADMIN_ROLE, L1_CONTRACT_CONTROLLER);
// dummyToken.renounceRole(DEFAULT_ADMIN_ROLE, DEPLOYER_ADDRESS);

address scrollReceiverImpl = address(new L1ScrollReceiverETHUpgradeable{salt: keccak256("ScrollReceiverImpl")}());
address scrollReceiverProxy = address(
new TransparentUpgradeableProxy{salt: keccak256("ScrollReceiver")}(
scrollReceiverImpl,
L1_TIMELOCK,
abi.encodeWithSelector(
L1ScrollReceiverETHUpgradeable.initialize.selector, L1_SYNC_POOL, SCROLL.L1_MESSENGER, L1_CONTRACT_CONTROLLER
)
)
);
console.log("ScrollReceiver deployed at: ", scrollReceiverProxy);
require(scrollReceiverProxy == SCROLL.L1_RECEIVER, "ScrollReceiver address mismatch");
// address scrollReceiverImpl = address(new L1ScrollReceiverETHUpgradeable{salt: keccak256("ScrollReceiverImpl")}());
// address scrollReceiverProxy = address(
// new TransparentUpgradeableProxy{salt: keccak256("ScrollReceiver")}(
// scrollReceiverImpl,
// L1_TIMELOCK,
// abi.encodeWithSelector(
// L1ScrollReceiverETHUpgradeable.initialize.selector, L1_SYNC_POOL, SCROLL.L1_MESSENGER, L1_CONTRACT_CONTROLLER
// )
// )
// );
// console.log("ScrollReceiver deployed at: ", scrollReceiverProxy);
// require(scrollReceiverProxy == SCROLL.L1_RECEIVER, "ScrollReceiver address mismatch");

console.log("Generating L1 transactions for native minting...");
// console.log("Generating L1 transactions for native minting...");

// the require transactions to integrate native minting on the L1 side are spilt between the timelock and the L1 contract controller

Expand All @@ -61,13 +61,13 @@ contract L1NativeMintingScript is Script, L2Constants, LayerZeroHelpers, GnosisH
string memory timelock_execute_transactions = _getGnosisHeader("1");

// registers the new dummy token as an acceptable token for the vamp contract
bytes memory setTokenData = abi.encodeWithSignature("registerToken(address,address,bool,uint16,uint32,uint32,bool)", dummyTokenProxy, address(0), true, 0, 20_000, 200_000, true);
bytes memory setTokenData = abi.encodeWithSignature("registerToken(address,address,bool,uint16,uint32,uint32,bool)", SCROLL.L1_DUMMY_TOKEN, address(0), true, 0, 20_000, 200_000, true);
timelock_schedule_transactions = string.concat(timelock_schedule_transactions, _getGnosisScheduleTransaction(L1_VAMP, setTokenData, false));
timelock_execute_transactions = string.concat(timelock_execute_transactions, _getGnosisExecuteTransaction(L1_VAMP, setTokenData, false));

// set {receiver, dummy} token on the L1 sync pool
bytes memory setReceiverData = abi.encodeWithSignature("setReceiver(uint32,address)", SCROLL.L2_EID, scrollReceiverProxy);
bytes memory setDummyTokenData = abi.encodeWithSignature("setDummyToken(uint32,address)", SCROLL.L2_EID, dummyTokenProxy);
bytes memory setReceiverData = abi.encodeWithSignature("setReceiver(uint32,address)", SCROLL.L2_EID, SCROLL.L1_RECEIVER);
bytes memory setDummyTokenData = abi.encodeWithSignature("setDummyToken(uint32,address)", SCROLL.L2_EID, SCROLL.L1_DUMMY_TOKEN);
timelock_schedule_transactions = string.concat(timelock_schedule_transactions, _getGnosisScheduleTransaction(L1_SYNC_POOL, setReceiverData, false));
timelock_schedule_transactions = string.concat(timelock_schedule_transactions, _getGnosisScheduleTransaction(L1_SYNC_POOL, setDummyTokenData, false));
timelock_execute_transactions = string.concat(timelock_execute_transactions, _getGnosisExecuteTransaction(L1_SYNC_POOL, setReceiverData, false));
Expand Down
7 changes: 4 additions & 3 deletions scripts/NativeMintingDeployment/DeployConfigureL2.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,11 @@ contract L2NativeMintingScript is Script, L2Constants, LayerZeroHelpers, GnosisH

console.log("Deploying contracts on L2...");

// Contracts are already deployed
// deploy and configure the native minting related contracts
address exchangeRateProvider = deployConfigureExchangeRateProvider(DEPLOYER_ADDRESS);
address rateLimiter = deployConfigureBucketRateLimiter(DEPLOYER_ADDRESS);
deployConfigureSyncPool(DEPLOYER_ADDRESS, exchangeRateProvider, rateLimiter);
// address exchangeRateProvider = deployConfigureExchangeRateProvider(DEPLOYER_ADDRESS);
// address rateLimiter = deployConfigureBucketRateLimiter(DEPLOYER_ADDRESS);
// deployConfigureSyncPool(DEPLOYER_ADDRESS, exchangeRateProvider, rateLimiter);

// generate the transactions required by the L2 contract controller

Expand Down
Loading