Skip to content

Commit

Permalink
Merge pull request #179 from maticnetwork/fix/mpt
Browse files Browse the repository at this point in the history
Fix/mpt
  • Loading branch information
ethyla authored Dec 4, 2024
2 parents bc15afd + 10f99d2 commit b48a3b4
Show file tree
Hide file tree
Showing 11 changed files with 42,600 additions and 119 deletions.
30 changes: 0 additions & 30 deletions .github/workflows/ci.yml

This file was deleted.

5 changes: 4 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ jobs:
- name: Install npm dependencies
run: npm install

- name: generate interfaces
run: npm run generate:interfaces

# - name: Run Forge fmt
# run: |
# forge fmt --check
Expand All @@ -54,5 +57,5 @@ jobs:

- name: Run Forge tests
run: |
forge test -vvv
forge test -vvv --no-match-test "SkipCI"
id: test
11 changes: 7 additions & 4 deletions contracts/lib/MerklePatriciaProof.sol
Original file line number Diff line number Diff line change
Expand Up @@ -69,16 +69,19 @@ library MerklePatriciaProof {
);
pathPtr += 1;
} else if (currentNodeList.length == 2) {
bytes memory nodeValue = RLPReader.toBytes(currentNodeList[0]);
uint256 traversed = _nibblesToTraverse(
RLPReader.toBytes(currentNodeList[0]),
nodeValue,
path,
pathPtr
);
//enforce correct nibble
bytes1 prefix = _getNthNibbleOfBytes(0, nodeValue);
if (pathPtr + traversed == path.length) {
//leaf node
if (
keccak256(RLPReader.toBytes(currentNodeList[1])) ==
keccak256(value)
keccak256(RLPReader.toBytes(currentNodeList[1])) == keccak256(value) &&
(prefix == bytes1(uint8(2)) || prefix == bytes1(uint8(3)))
) {
return true;
} else {
Expand All @@ -87,7 +90,7 @@ library MerklePatriciaProof {
}

//extension node
if (traversed == 0) {
if (traversed == 0 || (prefix != bytes1(uint8(0)) && prefix != bytes1(uint8(1)))) {
return false;
}

Expand Down
162 changes: 162 additions & 0 deletions forge/ExitPayloadReader.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
pragma solidity ^0.8.4;

import { RLPReader } from "./RLPReader.sol";

library ExitPayloadReader {
using RLPReader for bytes;
using RLPReader for RLPReader.RLPItem;

uint8 constant WORD_SIZE = 32;

struct ExitPayload {
RLPReader.RLPItem[] data;
}

struct Receipt {
RLPReader.RLPItem[] data;
bytes raw;
uint256 logIndex;
}

struct Log {
RLPReader.RLPItem data;
RLPReader.RLPItem[] list;
}

struct LogTopics {
RLPReader.RLPItem[] data;
}

// copy paste of private copy() from RLPReader to avoid changing of existing contracts
function copy(uint src, uint dest, uint len) private pure {
if (len == 0) return;

// copy as many word sizes as possible
for (; len >= WORD_SIZE; len -= WORD_SIZE) {
assembly {
mstore(dest, mload(src))
}

src += WORD_SIZE;
dest += WORD_SIZE;
}

// left over bytes. Mask is used to remove unwanted bytes from the word
uint mask = 256 ** (WORD_SIZE - len) - 1;
assembly {
let srcpart := and(mload(src), not(mask)) // zero out src
let destpart := and(mload(dest), mask) // retrieve the bytes
mstore(dest, or(destpart, srcpart))
}
}

function toExitPayload(bytes memory data)
internal
pure
returns (ExitPayload memory)
{
RLPReader.RLPItem[] memory payloadData = data
.toRlpItem()
.toList();

return ExitPayload(payloadData);
}

function getHeaderNumber(ExitPayload memory payload) internal pure returns(uint256) {
return payload.data[0].toUint();
}

function getBlockProof(ExitPayload memory payload) internal pure returns(bytes memory) {
return payload.data[1].toBytes();
}

function getBlockNumber(ExitPayload memory payload) internal pure returns(uint256) {
return payload.data[2].toUint();
}

function getBlockTime(ExitPayload memory payload) internal pure returns(uint256) {
return payload.data[3].toUint();
}

function getTxRoot(ExitPayload memory payload) internal pure returns(bytes32) {
return bytes32(payload.data[4].toUint());
}

function getReceiptRoot(ExitPayload memory payload) internal pure returns(bytes32) {
return bytes32(payload.data[5].toUint());
}

function getReceipt(ExitPayload memory payload) internal pure returns(Receipt memory receipt) {
receipt.raw = payload.data[6].toBytes();
RLPReader.RLPItem memory receiptItem = receipt.raw.toRlpItem();

if (receiptItem.isList()) {
// legacy tx
receipt.data = receiptItem.toList();
} else {
// pop first byte before parsting receipt
bytes memory typedBytes = receipt.raw;
bytes memory result = new bytes(typedBytes.length - 1);
uint256 srcPtr;
uint256 destPtr;
assembly {
srcPtr := add(33, typedBytes)
destPtr := add(0x20, result)
}

copy(srcPtr, destPtr, result.length);
receipt.data = result.toRlpItem().toList();
}

receipt.logIndex = getReceiptLogIndex(payload);
return receipt;
}

function getReceiptProof(ExitPayload memory payload) internal pure returns(bytes memory) {
return payload.data[7].toBytes();
}

function getBranchMaskAsBytes(ExitPayload memory payload) internal pure returns(bytes memory) {
return payload.data[8].toBytes();
}

function getBranchMaskAsUint(ExitPayload memory payload) internal pure returns(uint256) {
return payload.data[8].toUint();
}

function getReceiptLogIndex(ExitPayload memory payload) internal pure returns(uint256) {
return payload.data[9].toUint();
}

// Receipt methods
function toBytes(Receipt memory receipt) internal pure returns(bytes memory) {
return receipt.raw;
}

function getLog(Receipt memory receipt) internal pure returns(Log memory) {
RLPReader.RLPItem memory logData = receipt.data[3].toList()[receipt.logIndex];
return Log(logData, logData.toList());
}

// Log methods
function getEmitter(Log memory log) internal pure returns(address) {
return RLPReader.toAddress(log.list[0]);
}

function getTopics(Log memory log) internal pure returns(LogTopics memory) {
return LogTopics(log.list[1].toList());
}

function getData(Log memory log) internal pure returns(bytes memory) {
return log.list[2].toBytes();
}

function toRlpBytes(Log memory log) internal pure returns(bytes memory) {
return log.data.toRlpBytes();
}

// LogTopics methods
function getField(LogTopics memory topics, uint256 index) internal pure returns(RLPReader.RLPItem memory) {
return topics.data[index];
}
}
146 changes: 146 additions & 0 deletions forge/ForkupgradeMPT.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
pragma solidity ^0.8.4;

import {RootChainManagerProxy} from "../scripts/helpers/interfaces/RootChainManagerProxy.generated.sol";
import {RootChainManager} from "../scripts/helpers/interfaces/RootChainManager.generated.sol";

import {ExitPayloadReader} from "./ExitPayloadReader.sol";
import {MerklePatriciaProof} from "./MerklePatriciaProof.sol";

import "forge-std/Test.sol";

struct TxObject {
address from;
bytes32 hash;
bytes input;
bool isError;
bytes mod_input;
bool txreceipt_status;
}

struct FileObject {
TxObject[] txObjects;
}

contract ForkupgradeMPT is Test {
using stdJson for string;

uint256 mainnetFork;
address rootChainManagerProxy = 0xA0c68C638235ee32657e8f720a23ceC1bFc77C77;
address timelock = 0xCaf0aa768A3AE1297DF20072419Db8Bb8b5C8cEf;

bytes32 notOwner721Error = keccak256(hex"08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000294552433732313a207472616e73666572206f6620746f6b656e2074686174206973206e6f74206f776e0000000000000000000000000000000000000000000000");
bytes32 notOwnerNorApproved721Error = keccak256(hex"08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000314552433732313a207472616e736665722063616c6c6572206973206e6f74206f776e6572206e6f7220617070726f766564000000000000000000000000000000");
bytes32 outOfBalance20Error = keccak256(hex"08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002645524332303a205472616e7366657220616d6f756e7420657863656564732062616c616e63650000000000000000000000000000000000000000000000000000");
bytes32 exceedsBalance20Error = keccak256(hex"08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002645524332303a207472616e7366657220616d6f756e7420657863656564732062616c616e63650000000000000000000000000000000000000000000000000000");
bytes32 safeERC20lowlevelError = keccak256(hex"08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000205361666545524332303a206c6f772d6c6576656c2063616c6c206661696c6564");

function setUp() public {
mainnetFork = vm.createFork(vm.rpcUrl("mainnet"), 20910000);
vm.selectFork(mainnetFork);
}

function test_UpgradeMPTSkipCI() public {
assertEq(vm.activeFork(), mainnetFork);

address rootChainManagerImpl = deployCode("out/RootChainManager.sol/RootChainManager.json");

vm.prank(address(timelock));
RootChainManagerProxy(payable(rootChainManagerProxy)).updateImplementation(rootChainManagerImpl);

// load tx to be replayed
string memory txsJson = vm.readFile("forge/batchExit.json");
bytes memory txs = vm.parseJson(txsJson);
FileObject memory txBatch1 = abi.decode(txs, (FileObject));
uint256 successes = 0;
uint256 successesButError = 0;
uint256 intentionalError = 0;

// loop
for (uint i = 0; i < txBatch1.txObjects.length; i++) {
//for (uint i = 13; i < 15; i++) {
console.log("tx num: ", i);
TxObject memory obj = txBatch1.txObjects[i];
console.log("from: ", obj.from);
console.log("hash:");
console.logBytes32(obj.hash);
// Get the index of the exit
bytes32 index = calcExitHash(obj.mod_input);
console.logBytes32(index);

// Calculate the storage slots we need to manipulate to replay the tx
bytes32 slotprocessedExits = keccak256(abi.encode(index, 6));

// RootChainManager
vm.store(rootChainManagerProxy, slotprocessedExits, 0);

// Pretend to be the the exitor and replay tx
vm.prank(obj.from);
(bool successSchedule, bytes memory dataSchedule) = rootChainManagerProxy.call(obj.input);
if (successSchedule == false) {
if(obj.isError) {
console.log("successful failure");
intentionalError += 1;
continue;
}
if(keccak256(dataSchedule) == notOwner721Error) {
console.log("Not owner error");
successesButError += 1;
continue;
}
if(keccak256(dataSchedule) == notOwnerNorApproved721Error) {
console.log("Not owner error");
successesButError += 1;
continue;
}
if(keccak256(dataSchedule) == outOfBalance20Error) {
console.log("Not enough balance error");
successesButError += 1;
continue;
}
if(keccak256(dataSchedule) == exceedsBalance20Error) {
console.log("Exceeds balance error");
successesButError += 1;
continue;
}
if(keccak256(dataSchedule) == safeERC20lowlevelError){
console.log("Safe ERC20 low level fail");
successesButError += 1;
continue;
}

console.log("actual error");
console.logBytes(dataSchedule);
assembly {
revert(add(dataSchedule, 32), mload(dataSchedule))
}
} else {
successes += 1;
console.log("success");
}
}
console.log("Total successful tx: ", successes);
console.log("Total successful MPT but error tx: ", successesButError);
console.log("Total intentional error tx: ", intentionalError);

}

// taken from RootChainManager
function calcExitHash(bytes memory input) internal pure returns (bytes32) {
ExitPayloadReader.ExitPayload memory payload = ExitPayloadReader.toExitPayload(input);

bytes memory branchMaskBytes = ExitPayloadReader.getBranchMaskAsBytes(payload);
// checking if exit has already been processed
// unique exit is identified using hash of (blockNumber, branchMask, receiptLogIndex)
bytes32 exitHash = keccak256(
abi.encodePacked(
ExitPayloadReader.getBlockNumber(payload),
// first 2 nibbles are dropped while generating nibble array
// this allows branch masks that are valid but bypass exitHash check (changing first 2 nibbles only)
// so converting to nibble array and then hashing it
MerklePatriciaProof._getNibbleArray(branchMaskBytes),
ExitPayloadReader.getReceiptLogIndex(payload)
)
);
return exitHash;
}
}
Loading

0 comments on commit b48a3b4

Please sign in to comment.