Skip to content
Merged
Show file tree
Hide file tree
Changes from 53 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
e555918
chore: rebase contracts
Oct 31, 2025
c6c9af7
Merge pull request #436 from kleros/fix/handle-sent-snapshots
mani99brar Oct 31, 2025
3bcc672
chore: remove dev logs
Oct 31, 2025
643e814
fix: executeable condition
Oct 31, 2025
a17231d
fix: validate txn hash
Nov 3, 2025
d023e80
feat: docker setup
Nov 10, 2025
4dbb3c3
chore: remove contract workspace & add ether dep
Nov 10, 2025
1d3a917
chore: update relative imports
Nov 10, 2025
a2eb880
chore: conditional contract build
Nov 10, 2025
14d4495
chore: remove dev comments
Nov 10, 2025
6c2dae9
chore: refactor last msg query
Nov 10, 2025
26985af
fix: snapshot log index
Nov 10, 2025
317272b
feat: pino & struct logs
Nov 13, 2025
4b14b1d
chore: remove pm2
Nov 13, 2025
a3cc948
chore: update node version
Nov 28, 2025
e7dae43
feat: heartbeat
Nov 28, 2025
2ab1be0
fix: image node version
Nov 28, 2025
f16466f
feat: add inbox field in Message
Dec 1, 2025
2b4a749
chore: try catch for lastSavedMessage ql query
Dec 1, 2025
10e123e
fix: empty messages
Dec 2, 2025
a955ad8
fix: skip challenge when finality issue
Dec 2, 2025
eaadc7c
fix: update lastMessageResponse type
Dec 2, 2025
438bc01
feat(relayer): pino
Dec 5, 2025
4a0b833
fix: saveSnapshot comment
Dec 8, 2025
336b247
chore: batch ql claim query
Dec 8, 2025
d5a6f28
feat: saveSnapshot if challenged
Dec 11, 2025
785e2d4
chore: inc block range
Dec 12, 2025
60ba6cf
fix: finality logs & check
Dec 15, 2025
b33956c
chore: add finality return
Dec 15, 2025
ff8ee2b
chore: add pino dep
Dec 15, 2025
37528a1
chore: update ql url
Dec 15, 2025
9a6a626
fix: push latest claimable epoch
Dec 15, 2025
1c0d4db
chore: remove log & catch fallback
Dec 15, 2025
f0d3305
fix: block range offset & hard fallback
Dec 17, 2025
5e683ec
chore: rebase veascan subgraph fixes
Dec 17, 2025
e6df6fa
fix: seq offline flag & claim reinit
Dec 17, 2025
bde0780
chore: use final toBlock
Dec 19, 2025
7f7dbb4
chore: remove range for verification events
Dec 19, 2025
ab37c59
chore: remove last claim conditions
Dec 19, 2025
cb7f984
fix: add lastClaim try catch
Dec 19, 2025
b73cd89
fix: snapshot lastClaim try catch
Dec 19, 2025
0066b07
Merge branch 'dev' into chore/deploy
mani99brar Dec 31, 2025
6232598
fix: review changes
Jan 5, 2026
08d664f
chore: update readme
Jan 5, 2026
511e71c
feat: agnostic hashi executor
Jan 10, 2026
c159af8
chore: hashi executor cleanup
Jan 12, 2026
7964718
chore: update watcher cycle
Jan 12, 2026
508e557
chore: update env example & readme
Jan 12, 2026
43a7117
Merge branch 'dev' into chore/deploy
mani99brar Jan 13, 2026
d400265
Merge pull request #439 from kleros/feat/agnostic-hashi-executor
mani99brar Jan 13, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ enum ReceiverChains {
const paramsByChainId = {
GNOSIS_CHIADO: {
deposit: parseEther("0.1"),
epochPeriod: 1800, // 30 min
epochPeriod: 300, // 5 min
minChallengePeriod: 0, // 30 min
numEpochTimeout: 10000000000000, // never
amb: "0x8448E15d0e706C0298dECA99F0b4744030e59d7d", // https://docs.gnosischain.com/bridges/About%20Token%20Bridges/amb-bridge#key-contracts
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const paramsByChainId = {
ETHEREUM_SEPOLIA: {
deposit: parseEther("0.001"),
// Average happy path wait time is 1 hour (30 min, 90 min), happy path only
epochPeriod: 1800, // 30 min
epochPeriod: 300, // 5 min
minChallengePeriod: 0, // 0 min
numEpochTimeout: 10000000000000, // never
maxMissingBlocks: 10000000000000,
Expand Down
4 changes: 2 additions & 2 deletions contracts/deploy/01-outbox/01-chiado-to-arb-sepolia-outbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ enum ReceiverChains {
const paramsByChainId = {
ARBITRUM_SEPOLIA: {
deposit: parseEther("0.1"),
epochPeriod: 1800, // 30 min
challengePeriod: 0, // 30 min
epochPeriod: 300, // 5 min
challengePeriod: 0, // 0 min
numEpochTimeout: 10000000000000, // never
sequencerDelayLimit: 86400,
sequencerFutureLimit: 3600,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ enum SenderChains {

const paramsByChainId = {
ARBITRUM_SEPOLIA: {
epochPeriod: 1800, // 1 hour
epochPeriod: 300, // 5 min
},
HARDHAT: {
epochPeriod: 600, // 10 minutes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ enum SenderChains {

const paramsByChainId = {
ARBITRUM_SEPOLIA: {
epochPeriod: 1800, // 1 hour
epochPeriod: 300, // 5 min
},
HARDHAT: {
epochPeriod: 600, // 10 minutes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ enum SenderChains {

const paramsByChainId = {
GNOSIS_CHIADO: {
epochPeriod: 1800, // 30 minutes
epochPeriod: 300, // 5 minutes
amb: "0x8448E15d0e706C0298dECA99F0b4744030e59d7d", // https://docs.gnosischain.com/bridges/About%20Token%20Bridges/amb-bridge#key-contracts
},
HARDHAT: {
Expand Down
44 changes: 19 additions & 25 deletions contracts/deployments/arbitrumSepolia/VeaInboxArbToEthDevnet.json

Large diffs are not rendered by default.

42 changes: 18 additions & 24 deletions contracts/deployments/arbitrumSepolia/VeaInboxArbToEthTestnet.json

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

77 changes: 51 additions & 26 deletions contracts/deployments/chiado/VeaOutboxArbToGnosisDevnet.json

Large diffs are not rendered by default.

73 changes: 49 additions & 24 deletions contracts/deployments/chiado/VeaOutboxArbToGnosisTestnet.json

Large diffs are not rendered by default.

46 changes: 23 additions & 23 deletions contracts/deployments/sepolia/RouterArbToGnosisDevnet.json

Large diffs are not rendered by default.

46 changes: 23 additions & 23 deletions contracts/deployments/sepolia/RouterArbToGnosisTestnet.json

Large diffs are not rendered by default.

97 changes: 61 additions & 36 deletions contracts/deployments/sepolia/VeaOutboxArbToEthDevnet.json

Large diffs are not rendered by default.

95 changes: 60 additions & 35 deletions contracts/deployments/sepolia/VeaOutboxArbToEthTestnet.json

Large diffs are not rendered by default.

17 changes: 0 additions & 17 deletions contracts/hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,6 @@ const config: HardhatUserConfig = {
sepolia: "sepolia",
chiado: "chiado",
},
verify: {
etherscan: {
apiUrl: "https://api-sepolia.arbiscan.io",
apiKey: process.env.ARBISCAN_API_KEY,
},
},
},
arbitrum: {
chainId: 42161,
Expand All @@ -87,12 +81,6 @@ const config: HardhatUserConfig = {
mainnet: "mainnet",
gnosischain: "gnosischain",
},
verify: {
etherscan: {
apiUrl: "https://api.arbiscan.io/api",
apiKey: process.env.ARBISCAN_API_KEY,
},
},
},
// OUTBOX ---------------------------------------------------------------------------------------
chiado: {
Expand Down Expand Up @@ -121,11 +109,6 @@ const config: HardhatUserConfig = {
companionNetworks: {
arbitrumSepolia: "arbitrumSepolia",
},
verify: {
etherscan: {
apiUrl: "https://blockscout.com/gnosis",
},
},
},
sepolia: {
chainId: 11155111,
Expand Down
2 changes: 1 addition & 1 deletion contracts/src/arbitrumToEth/VeaInboxArbToEth.sol
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ contract VeaInboxArbToEth is IVeaInbox {
return oldCount;
}

/// @dev Saves snapshot of state root. Snapshots can be saved a maximum of once per epoch.
/// @dev Saves snapshot of state root. Snapshots can be called multiple times per active epoch.
/// `O(log(count))` where count number of messages in the inbox.
/// Note: See merkle tree docs for details how inbox manages state.
function saveSnapshot() external {
Expand Down
2 changes: 1 addition & 1 deletion contracts/src/arbitrumToGnosis/VeaInboxArbToGnosis.sol
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ contract VeaInboxArbToGnosis is IVeaInbox {
return oldCount;
}

/// @dev Saves snapshot of state root. Snapshots can be saved a maximum of once per epoch.
/// @dev Saves snapshot of state root. Snapshots can be saved multiple times per active epoch.
/// `O(log(count))` where count number of messages in the inbox.
/// Note: See merkle tree docs for details how inbox manages state.
function saveSnapshot() external {
Expand Down
2 changes: 1 addition & 1 deletion contracts/src/gnosisToArbitrum/VeaInboxGnosisToArb.sol
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ contract VeaInboxGnosisToArb is IVeaInbox {
return oldCount;
}

/// @dev Saves snapshot of state root. Snapshots can be saved a maximum of once per epoch.
/// @dev Saves snapshot of state root. Snapshots can be saved multiple times per active epoch.
/// `O(log(count))` where count number of messages in the inbox.
/// Note: See merkle tree docs for details how inbox manages state.
function saveSnapshot() external {
Expand Down
5 changes: 3 additions & 2 deletions contracts/src/test/ArbitrumToEth/VeaOutboxMockArbToEth.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ contract VeaOutboxMockArbToEth is VeaOutboxArbToEth {
bytes32 _stateRoot,
Claim memory _claim
) external override OnlyBridgeRunning {
require(claimHashes[_epoch] == hashClaim(_claim), "Invalid claim.");

require(msg.sender == address(arbSys), "Not from bridge.");

if (_epoch > latestVerifiedEpoch) {
Expand All @@ -42,6 +40,9 @@ contract VeaOutboxMockArbToEth is VeaOutboxArbToEth {
_claim.honest = Party.Challenger;
}
claimHashes[_epoch] = hashClaim(_claim);
emit Verified(_epoch);
} else {
emit FailedResolution(_epoch);
}
}

Expand Down
140 changes: 139 additions & 1 deletion contracts/test/integration/ArbToEth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -618,7 +618,145 @@ describe("Integration tests", async () => {
.withArgs(epoch, ethers.encodeBytes32String("")); // ticketId is always 0x00..0
});

it("challenger's deposit should be forfeited", async () => {
it("should be not able to resolve challenge for an invalid claim in fallback", async () => {
const data = 1121;

await senderGateway.sendMessage(data);
await veaInbox.connect(bridger).saveSnapshot();

const BatchOutgoing = veaInbox.filters.SnapshotSaved();
const batchOutGoingEvent = await veaInbox.queryFilter(BatchOutgoing);
const epochPeriod = Number(await veaInbox.epochPeriod());
const epoch = Math.floor((await batchOutGoingEvent[0].getBlock()).timestamp / epochPeriod);
const batchMerkleRoot = await veaInbox.snapshots(epoch);

await network.provider.send("evm_increaseTime", [epochPeriod]);
await network.provider.send("evm_mine");

// bridger tx starts - Honest Bridger
const bridgerClaimTx = await veaOutbox.connect(bridger).claim(epoch, batchMerkleRoot, { value: TEN_ETH });
const block = await ethers.provider.getBlock(bridgerClaimTx.blockNumber!);
if (!block) return;

const claim = {
stateRoot: batchMerkleRoot,
claimer: bridger.address,
timestampClaimed: block.timestamp,
timestampVerification: 0,
blocknumberVerification: 0,
honest: 0,
challenger: ethers.ZeroAddress,
};

await veaOutbox
.connect(challenger)
["challenge(uint256,(bytes32,address,uint32,uint32,uint32,uint8,address))"](epoch, claim, { value: TEN_ETH });

claim.challenger = challenger.address;
await expect(veaOutbox.startVerification(epoch, claim)).to.be.revertedWith("Claim is challenged.");

const claimHashBeforeFallback = await veaOutbox.claimHashes(epoch);
const maliciousClaim = { ...claim };
maliciousClaim.claimer = challenger.address;
maliciousClaim.challenger = bridger.address;
// Sending a malicious claim
const sendSafeFallbackTx = await veaInbox
.connect(bridger)
.sendSnapshot(epoch, maliciousClaim, { gasLimit: 1000000 });

const claimHashAfterFallback = await veaOutbox.claimHashes(epoch);
await expect(sendSafeFallbackTx)
.to.emit(veaInbox, "SnapshotSent")
.withArgs(epoch, ethers.encodeBytes32String(""));

// The FailedResolution event will also be emitted in the sendSafeFallbackTx due to mock behaviour
await expect(sendSafeFallbackTx).to.emit(veaOutbox, "FailedResolution").withArgs(epoch);

// This ensure that the claim is not resolved and still disputed
expect(claimHashAfterFallback).to.equal(claimHashBeforeFallback);
});

it("should not update latestEpoch and stateRoot when resolving older dispute", async () => {
// sending two messages
const data = 1121;
await senderGateway.sendMessage(data);
await veaInbox.connect(bridger).saveSnapshot();

const BatchOutgoing = veaInbox.filters.SnapshotSaved();
const batchOutGoingEvent = await veaInbox.queryFilter(BatchOutgoing);
const epochPeriod = Number(await veaInbox.epochPeriod());
const epoch = Math.floor((await batchOutGoingEvent[0].getBlock()).timestamp / epochPeriod);
const batchMerkleRoot = await veaInbox.snapshots(epoch);

await network.provider.send("evm_increaseTime", [epochPeriod]);
await network.provider.send("evm_mine");

// bridger tx starts - Honest Bridger
const bridgerClaimTx = await veaOutbox.connect(bridger).claim(epoch, batchMerkleRoot, { value: TEN_ETH });
const block = await ethers.provider.getBlock(bridgerClaimTx.blockNumber!);
if (!block) return;

await veaOutbox.connect(challenger)["challenge(uint256,(bytes32,address,uint32,uint32,uint32,uint8,address))"](
epoch,
{
stateRoot: batchMerkleRoot,
claimer: bridger.address,
timestampClaimed: block.timestamp,
timestampVerification: 0,
blocknumberVerification: 0,
honest: 0,
challenger: ethers.ZeroAddress,
},
{ value: TEN_ETH }
);

// save snapshot for new epoch
await veaInbox.connect(bridger).saveSnapshot();
// claim and verify the new epoch
await claimAndVerify({
veaInbox,
veaOutbox,
bridger,
epoch: epoch + 1,
batchMerkleRoot: await veaInbox.snapshots(epoch + 1),
ethers,
network,
mine,
});

// Send snapshot for the older disputed epoch
await veaInbox.connect(bridger).sendSnapshot(
epoch,
{
stateRoot: batchMerkleRoot,
claimer: bridger.address,
timestampClaimed: block.timestamp,
timestampVerification: 0,
blocknumberVerification: 0,
honest: 0,
challenger: challenger.address,
},
{ gasLimit: 1000000 }
);

// Ensure that the latest epoch and state root is not updated
expect(await veaOutbox.latestVerifiedEpoch()).to.equal(epoch + 1);

// Verify if the claim is resolved in favour of the bridger
const outboxClaimHash = await veaOutbox.claimHashes(epoch);
const expectedClaimHash = await veaOutbox.hashClaim({
stateRoot: batchMerkleRoot,
claimer: bridger.address,
timestampClaimed: block.timestamp,
timestampVerification: 0,
blocknumberVerification: 0,
honest: 1, // Dispute resolved in favour of claimer
challenger: challenger.address,
});
expect(outboxClaimHash).to.equal(expectedClaimHash);
});

it("challenger's deposit should be forfeited and bridger should be able to withdraw", async () => {
// sample data
const data = 1121;

Expand Down
44 changes: 44 additions & 0 deletions contracts/test/integration/ArbToGnosis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,50 @@ describe("Arbitrum to Gnosis Bridge Tests", async () => {
expect(await veaOutbox.latestVerifiedEpoch()).to.equal(epoch, "VeaOutbox latestVerifiedEpoch should be updated");
});

it("should not be able to resolve dispute with invalid claim in fallback", async () => {
const { claimBlock } = await setupClaimAndChallenge(epoch, batchMerkleRoot, 0);
const maliciousClaim = {
stateRoot: batchMerkleRoot,
claimer: challenger.address, // Switched Addresses
timestampClaimed: claimBlock.timestamp,
timestampVerification: 0,
blocknumberVerification: 0,
honest: 0,
challenger: bridger.address, // Switched Addresses
};
const claim = { ...maliciousClaim };
claim.claimer = bridger.address;
claim.challenger = challenger.address;

await veaInbox.connect(bridger).sendSnapshot(epoch, 100000, maliciousClaim, { gasLimit: 100000 });

const callData = await veaInbox.connect(bridger).getCallData(epoch, 100000, maliciousClaim);
const routerAddress = await router.getAddress();
await bridgeMock.connect(bridger).executeL1Message(routerAddress, callData);

await network.provider.send("evm_increaseTime", [CHALLENGE_PERIOD + SEQUENCER_DELAY]);
await network.provider.send("evm_mine");

await expect(veaOutbox.startVerification(epoch, claim)).to.be.revertedWith("Claim is challenged.");

const events = await amb.queryFilter(amb.filters.MockedEvent());
const lastEvent = events[events.length - 1];
const claimHashBeforeFallback = await veaOutbox.claimHashes(epoch);
const fallbackExecutionTx = await amb.executeMessageCall(
veaOutbox.target,
router.target,
lastEvent.args._data,
lastEvent.args.messageId,
1000000
);

// The FailedResolution event will also be emitted in the sendSafeFallbackTx due to mock behaviour
await expect(fallbackExecutionTx).to.emit(veaOutbox, "FailedResolution").withArgs(epoch);

const claimHashAfterFallback = await veaOutbox.claimHashes(epoch);
expect(claimHashAfterFallback).to.equal(claimHashBeforeFallback);
});

it("should not update latestEpoch and stateRoot when resolving older dispute", async () => {
const { claimBlock } = await setupClaimAndChallenge(epoch, batchMerkleRoot, 0);

Expand Down
25 changes: 25 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
services:
validator:
build:
context: .
dockerfile: ./validator-cli/Dockerfile
command: yarn start --saveSnapshot
env_file:
- path: ./validator-cli/.env
required: true
restart: unless-stopped
networks:
- vea-network

relayer:
build:
context: .
dockerfile: ./relayer-cli/Dockerfile
env_file:
- path: ./relayer-cli/.env
required: true
restart: unless-stopped
networks:
- vea-network
networks:
vea-network:
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
],
"packageManager": "yarn@4.6.0",
"volta": {
"node": "22.14.0",
"node": "24.14.0",
"yarn": "4.6.0"
},
"devDependencies": {
Expand Down
5 changes: 4 additions & 1 deletion relayer-cli/.env.dist
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,7 @@ TRANSACTION_BATCHER_CONTRACT_SEPOLIA=0xe7953da7751063d0a41ba727c32c762d3523ade8
TRANSACTION_BATCHER_CONTRACT_CHIADO=0xcC0a08D4BCC5f91ee9a1587608f7a2975EA75d73

# Ensure the path ends with a trailing slash ("/")
STATE_DIR="/home/user/vea/relayer-cli/state/"
STATE_DIR="/home/user/vea/relayer-cli/state/"

# Hashi Executor enabler
HASHI_EXECUTOR_ENABLED=true
Loading