Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
b8d8acd
Create initial state
nikeshnazareth Jul 22, 2025
a60d464
Create default message
nikeshnazareth Jul 22, 2025
d2aed97
Create message recipient
nikeshnazareth Jul 22, 2025
6b84dba
Create DepositRecipientScenarios
nikeshnazareth Jul 22, 2025
6205519
Create TipRecipientScenarios
nikeshnazareth Jul 22, 2025
c67ec39
Test claimDeposit path
nikeshnazareth Jul 22, 2025
9074f4d
add more scenarios
LeoPatOZ Jul 25, 2025
fa42d46
made scripts easier to work with
LeoPatOZ Jul 25, 2025
cc68261
reentrancy test
LeoPatOZ Jul 25, 2025
f0ee2db
remove uneeded counter
LeoPatOZ Jul 28, 2025
ffd4e33
fix scenarios
LeoPatOZ Jul 29, 2025
ac4471c
add more test scenarios
LeoPatOZ Jul 29, 2025
a3de214
better testing scenario
LeoPatOZ Jul 29, 2025
bd61c84
transient storage test
LeoPatOZ Jul 29, 2025
443b9e2
Merge branch 'main' into tests/message-relayer
LeoPatOZ Aug 1, 2025
f781997
update scenario
LeoPatOZ Aug 1, 2025
5347332
name
LeoPatOZ Aug 1, 2025
521fd25
Merge branch 'main' into tests/message-relayer
LeoPatOZ Aug 1, 2025
4608dc8
Create ifTxSucceeds modifier
nikeshnazareth Aug 5, 2025
28fb7bd
Remove InitialStateTest
nikeshnazareth Aug 5, 2025
d093a79
Use ifTxSucceeds so UserSetInvalidTipRecipient can inherit DepositRec…
nikeshnazareth Aug 5, 2025
0800551
Expand TipRecipientScenarios using the ifRelaySucceeds mechanism
nikeshnazareth Aug 5, 2025
0b1c6e7
Move shouldRevert checks to the InitialState contract
nikeshnazareth Aug 6, 2025
3b60423
Migrate FundAmountScenarios to the new structure
nikeshnazareth Aug 6, 2025
df1457e
Remove unused TIP_RECIPIENT_SLOT
nikeshnazareth Aug 6, 2025
9fb7b06
Test GasLimitScenarios
nikeshnazareth Aug 6, 2025
a0fed52
Migrate RelayRecipientScenarios to new structure
nikeshnazareth Aug 6, 2025
240c6d9
Create default scenarios for better encapsulation
nikeshnazareth Aug 6, 2025
4794e23
Fix InsufficientValue comment
nikeshnazareth Aug 6, 2025
7e30320
Run forge fmt
nikeshnazareth Aug 6, 2025
bac25c5
Increase OOG_INSIDE_RECIPIENT
nikeshnazareth Aug 6, 2025
d77e2ae
typo
LeoPatOZ Aug 6, 2025
0c38142
Merge branch 'main' into tests/message-relayer
nikeshnazareth Aug 6, 2025
9ff57d2
Set signalService to verify all signals
nikeshnazareth Aug 6, 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
9 changes: 4 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
[package]

name = "minimal-rollup"
version = "0.1.0"
edition = "2021"

[lib]
name = "minimal_rollup"
path = "offchain/lib.rs"

[[bin]]
name = "signal_slot"
path = "offchain/signal_slot.rs"

[[bin]]
name = "utils"
path = "offchain/utils.rs"

[[bin]]
name = "sample_signal_proof"
path = "offchain/sample_signal_proof.rs"
Expand Down
File renamed without changes.
49 changes: 39 additions & 10 deletions offchain/sample_deposit_proof.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
use alloy::primitives::utils::parse_units;
use alloy::sol_types::SolCall;
use eyre::Result;

mod signal_slot;
use signal_slot::get_signal_slot;

mod utils;
use utils::{deploy_eth_bridge, deploy_signal_service, get_proofs, get_provider, SignalProof};
use minimal_rollup::{
deploy_eth_bridge, deploy_signal_service, get_proofs, get_provider, SignalProof,
};

use alloy::hex::decode;
use alloy::hex::{self, decode};
use alloy::primitives::{address, Address, Bytes, FixedBytes, U256};
use std::fs;

use alloy::sol;

sol! {
function somePayableFunction(uint256 someArg) external payable returns (uint256);
function someNonpayableFunction(uint256 someArg) external returns (uint256);
}

fn expand_vector(vec: Vec<Bytes>, name: &str) -> String {
let mut expanded = String::new();
for (i, item) in vec.iter().enumerate() {
Expand All @@ -20,7 +30,7 @@ fn expand_vector(vec: Vec<Bytes>, name: &str) -> String {
return expanded;
}

fn create_deposit_call(
pub fn create_deposit_call(
proof: SignalProof,
nonce: usize,
signer: Address,
Expand Down Expand Up @@ -69,13 +79,31 @@ fn deposit_specification() -> Vec<DepositSpecification> {
// In this case, the CrossChainDepositExists.sol test case defines makeAddr("recipient");
let recipient = "0x006217c47ffA5Eb3F3c92247ffFE22AD998242c5";
// Use both zero and non-zero amounts (in this case 4 ether)
let amounts = vec![0_u128, 4000000000000000000_u128];
let amounts: Vec<U256> = vec![U256::ZERO, parse_units("4", "ether").unwrap().into()];

// Use different calldata to try different functions and inputs
let valid_payable_function_call = somePayableFunctionCall {
someArg: U256::from(1234),
}
.abi_encode();
let invalid_payable_function_call = somePayableFunctionCall {
someArg: U256::from(1235),
}
.abi_encode();
let valid_nonpayable_function_call = someNonpayableFunctionCall {
someArg: U256::from(1234),
}
.abi_encode();

let valid_payable_encoded = hex::encode(valid_payable_function_call);
let invalid_payable_encoded = hex::encode(invalid_payable_function_call);
let valid_nonpayable_encoded = hex::encode(valid_nonpayable_function_call);

let calldata = vec![
"", // empty
"9b28f6fb00000000000000000000000000000000000000000000000000000000000004d2", // (valid) call to somePayableFunction(1234)
"9b28f6fb00000000000000000000000000000000000000000000000000000000000004d3", // (invalid) call to somePayableFunction(1235)
"5932a71200000000000000000000000000000000000000000000000000000000000004d2", // (valid) call to `someNonPayableFunction(1234)`
"",
&valid_payable_encoded,
&invalid_payable_encoded,
&valid_nonpayable_encoded,
];

// makeAddr("canceler");
Expand Down Expand Up @@ -126,6 +154,7 @@ async fn main() -> Result<()> {
let deposits = deposit_specification();
assert!(deposits.len() > 0, "No deposits to prove");
let mut ids: Vec<FixedBytes<32>> = vec![];

// Perform all deposits
for (_i, spec) in deposits.iter().enumerate() {
let tx = eth_bridge
Expand Down Expand Up @@ -175,7 +204,7 @@ async fn main() -> Result<()> {
.as_str();
}

let template = fs::read_to_string("offchain/sample_deposit_proof.tmpl")?;
let template = fs::read_to_string("offchain/tmpl/sample_deposit_proof.tmpl")?;
let formatted = template
.replace(
"{signal_service_address}",
Expand Down
6 changes: 2 additions & 4 deletions offchain/sample_signal_proof.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ use eyre::Result;
mod signal_slot;
use signal_slot::get_signal_slot;

mod utils;
use utils::{deploy_signal_service, get_proofs, get_provider};
use minimal_rollup::{deploy_signal_service, get_proofs, get_provider};

use alloy::primitives::Bytes;
use std::fs;
Expand Down Expand Up @@ -32,7 +31,7 @@ async fn main() -> Result<()> {
let proof = get_proofs(&provider, slot, &signal_service).await?;


let template = fs::read_to_string("offchain/sample_proof.tmpl")?;
let template = fs::read_to_string("offchain/tmpl/sample_proof.tmpl")?;
let formatted = template
.replace("{signal_service_address}", signal_service.address().to_string().as_str())
.replace("{block_hash}", proof.block_hash.to_string().as_str())
Expand All @@ -47,4 +46,3 @@ async fn main() -> Result<()> {
println!("{}", formatted);
Ok(())
}

File renamed without changes.
3 changes: 3 additions & 0 deletions src/protocol/IMessageRelayer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ interface IMessageRelayer {
/// @dev Message forwarding failed
error MessageForwardingFailed();

/// @dev Value sent is lower than tip amount
error InsufficientValue();

/// @dev Tip transfer failed
error TipTransferFailed();

Expand Down
1 change: 1 addition & 0 deletions src/protocol/taiko_alethia/MessageRelayer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ contract MessageRelayer is ReentrancyGuardTransient, IMessageRelayer {
require(tipRecipient != address(0), NoTipRecipient());
}

require(msg.value >= tip, InsufficientValue());
uint256 valueToSend = msg.value - tip;
bool forwardMessageSuccess;

Expand Down
46 changes: 46 additions & 0 deletions test/MessageRelayer/DepositRecipientScenarios.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {GenericRecipient} from "./GenericRecipient.t.sol";
import {InitialState} from "./InitialState.t.sol";

import {IETHBridge} from "src/protocol/IETHBridge.sol";
import {IMessageRelayer} from "src/protocol/IMessageRelayer.sol";

abstract contract DepositRecipientScenarios is InitialState {}

contract DepositRecipientIsMessageRelayer is DepositRecipientScenarios {
function test_DepositRecipientIsMessageRelayer_relayMessage_shouldInvokeReceiveMessage() public ifRelaySucceeds {
vm.expectCall(address(messageRelayer), ethDeposit.data);
_relayMessage();
}

function test_DepositRecipientIsMessageRelayer_claimDeposit_shouldInvokeReceiveMessage() public ifClaimSucceeds {
vm.expectCall(address(messageRelayer), ethDeposit.data);
_claimDeposit();
}
}

contract DepositRecipientIsNotMessageRelayer is DepositRecipientScenarios {
function setUp() public override {
super.setUp();
// bypass the relayer and send the message directly to the recipient
// do not bother changing the default message encoding (to a `receiveMessage` function)
// because the recipient handles any message
ethDeposit.to = address(to);
}

function test_DepositRecipientIsNotMessageRelayer_relayMessage_shouldInvokeRecipient() public {
vm.expectEmit();
emit GenericRecipient.FunctionCalled();
_relayMessage();
}

function test_DepositRecipientIsNotMessageRelayer_relayMessage_shouldNotInvokeReceiveMessage() public {
vm.expectCall(address(messageRelayer), ethDeposit.data, 0);
_relayMessage();
}
}

// A valid scenario that can be used as a default scenario by unrelated tests.
abstract contract DefaultRecipientScenario is DepositRecipientScenarios {}
67 changes: 67 additions & 0 deletions test/MessageRelayer/FundAmountScenarios.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {GenericRecipient} from "./GenericRecipient.t.sol";
import {DefaultTipRecipientScenario} from "./TipRecipientScenarios.t.sol";
import {IMessageRelayer} from "src/protocol/IMessageRelayer.sol";

import {InitialState} from "./InitialState.t.sol";
import {IETHBridge} from "src/protocol/IETHBridge.sol";

abstract contract FundAmountScenarios is DefaultTipRecipientScenario {
function test_FundAmountScenarios_relayMessage_shouldInvokeRecipient() public ifRelaySucceeds {
vm.expectEmit();
emit GenericRecipient.FunctionCalled();
_relayMessage();
}

function test_FundAmountScenarios_relayMessage_shouldNotRetainFundsInRelayer() public ifRelaySucceeds {
assertEq(address(messageRelayer).balance, 0, "relayer should not have funds");
_relayMessage();
assertEq(address(messageRelayer).balance, 0, "relayer should not retain funds");
}

function test_FundAmountScenarios_relayMessage_shouldSendAmountToRecipient() public ifRelaySucceeds {
uint256 balanceBefore = address(to).balance;
uint256 transferAmount = ethDeposit.amount - tip;
_relayMessage();
assertEq(address(to).balance, balanceBefore + transferAmount, "recipient balance mismatch");
}

function redundant_FundAmountScenarios_relayMessage_shouldSendTipToRecipient() public {
// This test (if it were implemented) would be redundant with the tip recipient scenarios
// It is included for completeness, so this file accounts for all the distributed funds
}
}

contract AmountExceedsTip is FundAmountScenarios {}

contract NoAmountNoTip is FundAmountScenarios {
function setUp() public override {
super.setUp();
ethDeposit.amount = 0;
tip = 0;
_encodeReceiveCall();
}
}

contract NoAmountNonzeroTip is FundAmountScenarios {
function setUp() public override {
super.setUp();
ethDeposit.amount = 0;
relayShouldSucceed = false;
claimShouldSucceed = false;
}
}

contract AmountLessThanTip is FundAmountScenarios {
function setUp() public override {
super.setUp();
ethDeposit.amount = tip - 1 wei;
relayShouldSucceed = false;
claimShouldSucceed = false;
}
}

// A valid scenario that can be used as a default scenario by unrelated tests.
abstract contract DefaultFundAmountScenario is AmountExceedsTip {}
59 changes: 59 additions & 0 deletions test/MessageRelayer/GasLimitScenarios.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {DefaultFundAmountScenario} from "./FundAmountScenarios.t.sol";
import {GenericRecipient} from "./GenericRecipient.t.sol";

import {InitialState} from "./InitialState.t.sol";
import {IETHBridge} from "src/protocol/IETHBridge.sol";
import {IMessageRelayer} from "src/protocol/IMessageRelayer.sol";

// Found by experimentation
uint256 constant OOG_INSIDE_RECIPIENT = 70_000;

abstract contract GasLimitScenarios is DefaultFundAmountScenario {}

contract NoGasLimit_SufficientGasProvided is GasLimitScenarios {}

contract NoGasLimit_InsufficientGasProvided is GasLimitScenarios {
function setUp() public override {
super.setUp();
gasProvidedWithCall = OOG_INSIDE_RECIPIENT;
relayShouldSucceed = false;
claimShouldSucceed = false;
}
}

contract SufficientGasLimit_SufficientGasProvided is GasLimitScenarios {
function setUp() public override {
super.setUp();
gasLimit = to.GAS_REQUIRED() + 100;
_encodeReceiveCall();
}
}

contract SufficientGasLimit_InsufficientGasProvided is GasLimitScenarios {
function setUp() public override {
super.setUp();
gasLimit = to.GAS_REQUIRED() + 100;
_encodeReceiveCall();
gasProvidedWithCall = OOG_INSIDE_RECIPIENT;
relayShouldSucceed = false;
claimShouldSucceed = false;
}
}

contract InsufficientGasLimit_SufficientGasProvided is GasLimitScenarios {
function setUp() public override {
super.setUp();
// the amount forwarded to the recipient is slightly higher than gasLimit so deduct 150 as compensation
// TODO: understand why this is necessary
gasLimit = to.GAS_REQUIRED() - 150;
_encodeReceiveCall();
relayShouldSucceed = false;
claimShouldSucceed = false;
}
}

// A valid scenario that can be used as a default scenario by unrelated tests.
abstract contract DefaultGasLimitScenario is NoGasLimit_SufficientGasProvided {}
53 changes: 53 additions & 0 deletions test/MessageRelayer/GenericRecipient.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {IMessageRelayer} from "src/protocol/IMessageRelayer.sol";

interface IGenericRecipient {
function setSuccess(bool _callWillSucceed) external;
function setReentrancyAttack(bool _shouldAttack) external;
}

contract GenericRecipient is IGenericRecipient {
bool private callWillSucceed = true;
bool private shouldReenterAttack = false;
address private relayer;

// Consume a minimum amount of gas so we can test gas limits
uint256 public constant GAS_REQUIRED = 20_000;

error CallFailed();

event FunctionCalled();

constructor(address _relayer) {
relayer = _relayer;
}

function setSuccess(bool _callWillSucceed) external {
callWillSucceed = _callWillSucceed;
}

function setReentrancyAttack(bool _shouldAttack) external {
shouldReenterAttack = _shouldAttack;
}

fallback() external payable {
_simulateFunctionCall();
}

receive() external payable {
_simulateFunctionCall();
}

function _simulateFunctionCall() internal {
require(callWillSucceed, CallFailed());
require(gasleft() >= GAS_REQUIRED, "Insufficient gas");

emit FunctionCalled();

if (shouldReenterAttack) {
IMessageRelayer(relayer).receiveMessage(address(this), 0, address(this), 0, "0x");
}
}
}
Loading