diff --git a/.github/workflows/rust.yml b/.github/workflows/checks.yml similarity index 90% rename from .github/workflows/rust.yml rename to .github/workflows/checks.yml index f2cc5b8dd..4c7f8df01 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/checks.yml @@ -1,4 +1,4 @@ -name: Rust +name: Checks # On Rust, GitHub Actions, and caching # =========== @@ -50,6 +50,8 @@ on: env: CARGO_TERM_COLOR: always RUSTFLAGS: -D warnings + FOUNDRY_PROFILE: ci + # Automatically cancels a job if a new commit if pushed to the same PR, branch, or tag. # Source: @@ -170,3 +172,32 @@ jobs: run: rustup show - name: Run nextest run: SKIP_GUEST_BUILD=1 make test + + system-contracts: + strategy: + fail-fast: true + + name: Foundry project + runs-on: ubicloud-standard-2 + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Run Forge build + run: | + cd module-system/module-implementations/sov-evm/src/evm/system_contracts + forge --version + forge build --sizes + id: build + + - name: Run Forge tests + run: | + cd module-system/module-implementations/sov-evm/src/evm/system_contracts + forge test -vvv + id: test \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..16945c69b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "module-system/module-implementations/sov-evm/src/evm/system_contracts/lib/forge-std"] + path = module-system/module-implementations/sov-evm/src/evm/system_contracts/lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/module-system/module-implementations/sov-evm/src/evm/system_contracts/.gitignore b/module-system/module-implementations/sov-evm/src/evm/system_contracts/.gitignore new file mode 100644 index 000000000..85198aaa5 --- /dev/null +++ b/module-system/module-implementations/sov-evm/src/evm/system_contracts/.gitignore @@ -0,0 +1,14 @@ +# Compiler files +cache/ +out/ + +# Ignores development broadcast logs +!/broadcast +/broadcast/*/31337/ +/broadcast/**/dry-run/ + +# Docs +docs/ + +# Dotenv file +.env diff --git a/module-system/module-implementations/sov-evm/src/evm/system_contracts/README.md b/module-system/module-implementations/sov-evm/src/evm/system_contracts/README.md new file mode 100644 index 000000000..9265b4558 --- /dev/null +++ b/module-system/module-implementations/sov-evm/src/evm/system_contracts/README.md @@ -0,0 +1,66 @@ +## Foundry + +**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** + +Foundry consists of: + +- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). +- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. +- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. +- **Chisel**: Fast, utilitarian, and verbose solidity REPL. + +## Documentation + +https://book.getfoundry.sh/ + +## Usage + +### Build + +```shell +$ forge build +``` + +### Test + +```shell +$ forge test +``` + +### Format + +```shell +$ forge fmt +``` + +### Gas Snapshots + +```shell +$ forge snapshot +``` + +### Anvil + +```shell +$ anvil +``` + +### Deploy + +```shell +$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key +``` + +### Cast + +```shell +$ cast +``` + +### Help + +```shell +$ forge --help +$ anvil --help +$ cast --help +``` diff --git a/module-system/module-implementations/sov-evm/src/evm/system_contracts/foundry.toml b/module-system/module-implementations/sov-evm/src/evm/system_contracts/foundry.toml new file mode 100644 index 000000000..25b918f9c --- /dev/null +++ b/module-system/module-implementations/sov-evm/src/evm/system_contracts/foundry.toml @@ -0,0 +1,6 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/module-system/module-implementations/sov-evm/src/evm/system_contracts/lib/Ownable.sol b/module-system/module-implementations/sov-evm/src/evm/system_contracts/lib/Ownable.sol new file mode 100644 index 000000000..1edf917d4 --- /dev/null +++ b/module-system/module-implementations/sov-evm/src/evm/system_contracts/lib/Ownable.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +abstract contract Ownable { + address public owner; + address public pendingOwner; + + event OwnershipTransferred(address previousOwner, address newOwner); + event OwnershipTransferRequested(address previousOwner, address newOwner); + + modifier onlyOwner() { + require(msg.sender == owner, "Caller is not owner"); + _; + } + + modifier onlyPendingOwner() { + require(msg.sender == pendingOwner, "Caller is not pending owner"); + _; + } + + constructor() { + owner = msg.sender; + } + + function renounceOwnership() public onlyOwner { + owner = address(0); + emit OwnershipTransferred(owner, address(0)); + } + + function transferOwnership(address newOwner) public onlyOwner { + pendingOwner = newOwner; + emit OwnershipTransferRequested(owner, newOwner); + } + + function acceptOwnership() public onlyPendingOwner { + address old_owner = owner; + owner = pendingOwner; + pendingOwner = address(0); + emit OwnershipTransferred(old_owner, pendingOwner); + } +} \ No newline at end of file diff --git a/module-system/module-implementations/sov-evm/src/evm/system_contracts/lib/forge-std b/module-system/module-implementations/sov-evm/src/evm/system_contracts/lib/forge-std new file mode 160000 index 000000000..ae570fec0 --- /dev/null +++ b/module-system/module-implementations/sov-evm/src/evm/system_contracts/lib/forge-std @@ -0,0 +1 @@ +Subproject commit ae570fec082bfe1c1f45b0acca4a2b4f84d345ce diff --git a/module-system/module-implementations/sov-evm/src/evm/system_contracts/src/L1BlockHashList.sol b/module-system/module-implementations/sov-evm/src/evm/system_contracts/src/L1BlockHashList.sol new file mode 100644 index 000000000..99eacc163 --- /dev/null +++ b/module-system/module-implementations/sov-evm/src/evm/system_contracts/src/L1BlockHashList.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import "../lib/Ownable.sol"; +import "./interfaces/IL1BlockHashList.sol"; + +/// @title A system contract that stores block hashes and merkle roots of L1 blocks +/// @author Citrea + +contract L1BlockHashList is Ownable, IL1BlockHashList { + mapping(uint256 => bytes32) public blockHashes; + mapping(bytes32 => bytes32) public merkleRoots; + uint256 public blockNumber; + + event BlockInfoAdded(uint256 blockNumber, bytes32 blockHash, bytes32 merkleRoot); + constructor() Ownable(){ } + + /// @notice Sets the initial value for the block number, can only be called once + /// @param _blockNumber The L1 block number that is associated with the genesis block of Citrea + function initializeBlockNumber(uint256 _blockNumber) public onlyOwner { + require(blockNumber == 0, "Already initialized"); + blockNumber = _blockNumber; + } + + /// @notice Sets the block hash and merkle root for a given block + /// @notice Can only be called after the initial block number is set + /// @dev The block number is incremented by the contract as no block info should be overwritten or skipped + /// @param _blockHash The hash of the current L1 block + /// @param _merkleRoot The merkle root of the current L1 block + function setBlockInfo(bytes32 _blockHash, bytes32 _merkleRoot) public onlyOwner { + uint256 _blockNumber = blockNumber; + require(_blockNumber != 0, "Not initialized"); + blockHashes[_blockNumber] = _blockHash; + blockNumber = _blockNumber + 1; + merkleRoots[_blockHash] = _merkleRoot; + emit BlockInfoAdded(blockNumber, _blockHash, _merkleRoot); + } + + /// @param _blockNumber The number of the block to get the hash for + /// @return The block hash for the given block + function getBlockHash(uint256 _blockNumber) public view returns (bytes32) { + return blockHashes[_blockNumber]; + } + + /// @param _blockHash The block hash of the block to get the merkle root for + /// @return The merkle root for the given block + function getMerkleRootByHash(bytes32 _blockHash) public view returns (bytes32) { + return merkleRoots[_blockHash]; + } + + /// @param _blockNumber The block number of the block to get the merkle root for + /// @return The merkle root for the given block + function getMerkleRootByNumber(uint256 _blockNumber) public view returns (bytes32) { + return merkleRoots[blockHashes[_blockNumber]]; + } +} diff --git a/module-system/module-implementations/sov-evm/src/evm/system_contracts/src/interfaces/IL1BlockHashList.sol b/module-system/module-implementations/sov-evm/src/evm/system_contracts/src/interfaces/IL1BlockHashList.sol new file mode 100644 index 000000000..06da7b7d7 --- /dev/null +++ b/module-system/module-implementations/sov-evm/src/evm/system_contracts/src/interfaces/IL1BlockHashList.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +interface IL1BlockHashList { + function initializeBlockNumber(uint256) external; + function setBlockInfo(bytes32, bytes32) external; + function getBlockHash(uint256) external view returns (bytes32); + function getMerkleRootByHash(bytes32) external view returns (bytes32); + function getMerkleRootByNumber(uint256) external view returns (bytes32); +} \ No newline at end of file diff --git a/module-system/module-implementations/sov-evm/src/evm/system_contracts/test/L1BlockHashList.t.sol b/module-system/module-implementations/sov-evm/src/evm/system_contracts/test/L1BlockHashList.t.sol new file mode 100644 index 000000000..fa6204a8a --- /dev/null +++ b/module-system/module-implementations/sov-evm/src/evm/system_contracts/test/L1BlockHashList.t.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import "../src/L1BlockHashList.sol"; + +contract L1BlockHashListTest is Test { + L1BlockHashList l1BlockHashList; + bytes32 randomBlockHash = bytes32(keccak256("CITREA_TEST")); + bytes32 randomMerkleRoot = bytes32(keccak256("CITREA")); + uint256 constant INITIAL_BLOCK_NUMBER = 505050; + + function setUp() public { + l1BlockHashList = new L1BlockHashList(); + } + + function testSetBlockInfo() public { + l1BlockHashList.initializeBlockNumber(INITIAL_BLOCK_NUMBER); + l1BlockHashList.setBlockInfo(randomBlockHash, randomMerkleRoot); + assertEq(l1BlockHashList.getBlockHash(INITIAL_BLOCK_NUMBER), randomBlockHash); + assertEq(l1BlockHashList.getMerkleRootByHash(randomBlockHash), randomMerkleRoot); + assertEq(l1BlockHashList.getMerkleRootByNumber(INITIAL_BLOCK_NUMBER), randomMerkleRoot); + } + + function testCannotReinitalize() public { + l1BlockHashList.initializeBlockNumber(INITIAL_BLOCK_NUMBER); + vm.expectRevert("Already initialized"); + l1BlockHashList.initializeBlockNumber(INITIAL_BLOCK_NUMBER - 10); + } + + function testNonOwnerCannotSetBlockInfo() public { + l1BlockHashList.initializeBlockNumber(INITIAL_BLOCK_NUMBER); + vm.startPrank(address(0x1)); + vm.expectRevert("Caller is not owner"); + l1BlockHashList.setBlockInfo(randomBlockHash, randomMerkleRoot); + } + + function testNonOwnerCannotInitializeBlockNumber() public { + vm.startPrank(address(0x1)); + vm.expectRevert("Caller is not owner"); + l1BlockHashList.initializeBlockNumber(INITIAL_BLOCK_NUMBER); + } + + function testCannotSetInfoWithoutInitialize() public { + vm.expectRevert("Not initialized"); + l1BlockHashList.setBlockInfo(randomBlockHash, randomMerkleRoot); + } + + function testBlockInfoAvailableAfterManyWrites() public { + l1BlockHashList.initializeBlockNumber(INITIAL_BLOCK_NUMBER); + for (uint256 i = 0; i < 100; i++) { + bytes32 blockHash = keccak256(abi.encodePacked(i)); + bytes32 root = keccak256(abi.encodePacked(blockHash)); + l1BlockHashList.setBlockInfo(blockHash, root); + assertEq(l1BlockHashList.getBlockHash(i + INITIAL_BLOCK_NUMBER), blockHash); + assertEq(l1BlockHashList.getMerkleRootByHash(blockHash), root); + assertEq(l1BlockHashList.getMerkleRootByNumber(i + INITIAL_BLOCK_NUMBER), root); + } + + bytes32 zeroth_hash = keccak256(abi.encodePacked(uint(0))); + bytes32 zeroth_root = keccak256(abi.encodePacked(zeroth_hash)); + assertEq(l1BlockHashList.getBlockHash(INITIAL_BLOCK_NUMBER), zeroth_hash); + assertEq(l1BlockHashList.getMerkleRootByHash(zeroth_hash), zeroth_root); + assertEq(l1BlockHashList.getMerkleRootByNumber(INITIAL_BLOCK_NUMBER), zeroth_root); + } +} \ No newline at end of file diff --git a/module-system/module-implementations/sov-evm/src/evm/system_contracts/test/Ownable.t.sol b/module-system/module-implementations/sov-evm/src/evm/system_contracts/test/Ownable.t.sol new file mode 100644 index 000000000..e5f7280b1 --- /dev/null +++ b/module-system/module-implementations/sov-evm/src/evm/system_contracts/test/Ownable.t.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import "../lib/Ownable.sol"; +import "forge-std/Test.sol"; + +contract OwnableHarness is Ownable { + constructor () Ownable() { } + + function privilegedFunction() public onlyOwner { + } +} + +contract OwnableTest is Test { + OwnableHarness ownable; + + function setUp() public { + ownable = new OwnableHarness(); + } + + function testOnlyOwner() public { + ownable.privilegedFunction(); + address non_owner = address(0x1); + vm.startPrank(non_owner); + vm.expectRevert("Caller is not owner"); + ownable.privilegedFunction(); + } + + function testTransferOwnership() public { + ownable.transferOwnership(address(0x1)); + assertEq(ownable.pendingOwner(), address(0x1)); + } + + function testAcceptOwnership() public { + address new_owner = address(0x1); + ownable.transferOwnership(new_owner); + vm.startPrank(new_owner); + ownable.acceptOwnership(); + assertEq(ownable.owner(), new_owner); + } + + function testRenounceOwnership() public { + ownable.renounceOwnership(); + assertEq(ownable.owner(), address(0)); + } +} \ No newline at end of file