datahaven/contracts/test/SnowbridgeIntegration.t.sol
Steve Degosserie 387c056912
fix: Resolve Foundry build errors and apply code formatting (#241)
## Summary

Fixes the CI build failure in the `task-ts-build` workflow caused by
Foundry v1.4.2's Solar linter not being able to resolve Snowbridge's
context-specific import remappings.

## Problem

The Snowbridge submodule uses context-specific remappings (prefixed with
`:`) for its dependencies:
- `lib/snowbridge/contracts/:openzeppelin/` → OpenZeppelin contracts
- `lib/snowbridge/contracts/:prb/math/` → PRB Math library

Foundry v1.4.2's Solar linter doesn't understand these context-specific
remappings and fails with errors like:
```
error: file openzeppelin/utils/cryptography/MerkleProof.sol not found
error: file prb/math/src/UD60x18.sol not found
```

## Solution

Added global remappings that the linter can understand:
```toml
"openzeppelin/=lib/snowbridge/contracts/lib/openzeppelin-contracts/contracts/",
"prb/math/=lib/snowbridge/contracts/lib/prb-math/",
```

### Why This Works
- The linter can now resolve `openzeppelin/` and `prb/math/` imports
globally
- These global remappings take **lower precedence** than
context-specific ones during compilation
- The compiler still uses the context-specific remappings (with `:`)
when compiling Snowbridge contracts
- The linter uses the global remappings when checking all files

## Changes

### Commit 1: Add global remappings
- `contracts/foundry.toml`: Added 2 global remapping entries

### Commit 2: Apply forge fmt
- Applied automatic formatting via `forge fmt` to ensure code style
consistency
- Multi-line formatting for long import statements and function
signatures
- No functional changes - purely formatting updates

## Testing

 Local build succeeds with `forge build`
 No Snowbridge import resolution errors
 `forge fmt --check` passes with no formatting issues
 Only linting notes/warnings remain (not errors)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-10-20 11:20:59 +03:00

406 lines
16 KiB
Solidity

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
/* solhint-disable func-name-mixedcase */
import {InboundMessageV2} from "snowbridge/src/Types.sol";
import {CommandV2, CommandKind, IGatewayV2} from "snowbridge/src/Types.sol";
import {
CallContractParams,
Payload,
Message,
MessageKind,
Asset,
AssetKind
} from "snowbridge/src/v2/Types.sol";
import {BeefyVerification} from "snowbridge/src/BeefyVerification.sol";
import {BeefyClient} from "snowbridge/src/BeefyClient.sol";
import {
IAllocationManager
} from "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol";
import {OperatorSet} from "eigenlayer-contracts/src/contracts/libraries/OperatorSetLib.sol";
import {MerkleUtils} from "../src/libraries/MerkleUtils.sol";
import {
IRewardsRegistryEvents,
IRewardsRegistryErrors
} from "../src/interfaces/IRewardsRegistry.sol";
import {SnowbridgeAndAVSDeployer} from "./utils/SnowbridgeAndAVSDeployer.sol";
import {ScaleCodec} from "snowbridge/src/utils/ScaleCodec.sol";
import "forge-std/Test.sol";
contract SnowbridgeIntegrationTest is SnowbridgeAndAVSDeployer {
// Storage variables to reduce stack depth
uint128[] internal _validatorPoints;
address[] internal _validatorAddresses;
bytes32 internal _validatorPointsMerkleRoot;
function setUp() public {
_deployMockAllContracts();
}
function beforeTestSetup(
bytes4 testSelector
) public pure returns (bytes[] memory beforeTestCalldata) {
if (testSelector == this.test_sendNewValidatorsSetMessage.selector) {
beforeTestCalldata = new bytes[](1);
beforeTestCalldata[0] = abi.encodeWithSelector(this.setupValidatorsAsOperators.selector);
}
}
function test_constructor() public view {
assertEq(
rewardsRegistry.rewardsAgent(),
address(rewardsAgent),
"Rewards agent address should be set correctly"
);
assertEq(
gateway.agentOf(REWARDS_MESSAGE_ORIGIN),
address(rewardsAgent),
"Rewards agent should be set correctly"
);
}
function test_newRewardsMessage() public {
// Setup validator data.
_setupValidatorData();
// Create and submit the rewards message.
InboundMessageV2 memory updateRewardsMessage = _createRewardsMessage();
// Build messages merkle tree
// We want a proof of the first message, i.e. the actual rewards message
bytes32[] memory messagesProof =
_buildMessagesProofForGoodRewardsMessage(updateRewardsMessage);
// Create BEEFY proof.
BeefyVerification.Proof memory beefyProof = _createBeefyProof();
// This is to mock that the `BeefyClient.verifyMMRLeafProof` function returns true
// despite the fact that we never registered a BEEFY leaf with this message in the
// `BeefyClient` contract.
_mockBeefyVerification();
// Submit message to Gateway.
// We don't care about the rewardAddress that will get the Snowbridge rewards for relaying this message.
bytes32 rewardAddress = keccak256(abi.encodePacked("rewardAddress"));
vm.expectEmit(address(gateway));
emit IGatewayV2.InboundMessageDispatched(0, bytes32(0), true, rewardAddress);
gateway.v2_submit(updateRewardsMessage, messagesProof, beefyProof, rewardAddress);
// Fund the RewardsRegistry to be able to distribute rewards
vm.deal(address(rewardsRegistry), 1000000 ether);
// Build proof for the first validator to claim rewards.
bytes32[] memory rewardsProofFirstValidator =
_buildValidatorPointsProof(_validatorAddresses, _validatorPoints, 0);
// Claim rewards for the first validator.
vm.mockCall(
address(allocationManager),
abi.encodeWithSelector(IAllocationManager.isMemberOfOperatorSet.selector),
abi.encode(true)
);
vm.startPrank(_validatorAddresses[0]);
vm.expectEmit(address(rewardsRegistry));
emit IRewardsRegistryEvents.RewardsClaimedForIndex(
_validatorAddresses[0], 0, _validatorPoints[0], uint256(_validatorPoints[0])
);
serviceManager.claimLatestOperatorRewards(
0, _validatorPoints[0], 10, 0, rewardsProofFirstValidator
);
vm.stopPrank();
// Check that the validator has received the rewards.
assertEq(
address(_validatorAddresses[0]).balance,
_validatorPoints[0],
"Validator should receive rewards"
);
// Build proof for the last validator to claim rewards.
bytes32[] memory rewardsProofLastValidator =
_buildValidatorPointsProof(_validatorAddresses, _validatorPoints, 9);
// Claim rewards for the last validator.
vm.startPrank(_validatorAddresses[9]);
vm.expectEmit(address(rewardsRegistry));
emit IRewardsRegistryEvents.RewardsClaimedForIndex(
_validatorAddresses[9], 0, _validatorPoints[9], uint256(_validatorPoints[9])
);
serviceManager.claimLatestOperatorRewards(
0, _validatorPoints[9], 10, 9, rewardsProofLastValidator
);
vm.stopPrank();
// Check that the last validator has received the rewards.
assertEq(
address(_validatorAddresses[9]).balance,
_validatorPoints[9],
"Last validator should receive rewards"
);
}
function test_newRewardsMessage_OnlyRewardsAgent() public {
// Setup validator data.
_setupValidatorData();
// Create and submit the rewards message.
InboundMessageV2 memory updateRewardsMessage = _createRewardsMessage();
// Build messages merkle tree.
// We want a proof of the third message, i.e. the attempt at setting the new rewards root
// with a wrong origin.
(InboundMessageV2 memory badUpdateRewardsMessage, bytes32[] memory messagesProof) =
_buildMessagesProofForBadRewardsMessage(updateRewardsMessage);
// Create BEEFY proof.
BeefyVerification.Proof memory beefyProof = _createBeefyProof();
// This is to mock that the `BeefyClient.verifyMMRLeafProof` function returns true
// despite the fact that we never registered a BEEFY leaf with this message in the
// `BeefyClient` contract.
_mockBeefyVerification();
// Submit message to Gateway.
// We don't care about the rewardAddress that will get the Snowbridge rewards for relaying this message.
// We expect this to fail in the RewardsRegistry contract because the Agent trying to
// set the new rewards root is not the authorised Agent. Therefore there should be an
// event emitted by the Gateway saying that the message was dispatched but it failed.
bytes32 rewardAddress = keccak256(abi.encodePacked("rewardAddress"));
emit IGatewayV2.InboundMessageDispatched(0, bytes32(0), false, rewardAddress);
gateway.v2_submit(badUpdateRewardsMessage, messagesProof, beefyProof, rewardAddress);
}
function test_sendNewValidatorsSetMessage() public {
// Check that the current validators signed as operators have a registered address for the DataHaven solochain.
address[] memory currentOperators = allocationManager.getMembers(
OperatorSet({avs: address(serviceManager), id: serviceManager.VALIDATORS_SET_ID()})
);
for (uint256 i = 0; i < currentOperators.length; i++) {
assertEq(
serviceManager.validatorEthAddressToSolochainAddress(currentOperators[i]),
address(uint160(uint256(initialValidatorHashes[i]))),
"Validator should have a registered address for the DataHaven solochain"
);
}
// Mock balance for the AVS owner
vm.deal(avsOwner, 1000000 ether);
// Send the new validator set message to the Snowbridge Gateway
bytes memory message = serviceManager.buildNewValidatorSetMessage();
Payload memory payload = Payload({
origin: address(serviceManager),
assets: new Asset[](0),
message: Message({kind: MessageKind.Raw, data: message}),
claimer: bytes(""),
value: 0,
executionFee: 1 ether,
relayerFee: 1 ether
});
cheats.expectEmit();
emit IGatewayV2.OutboundMessageAccepted(1, payload);
cheats.prank(avsOwner);
serviceManager.sendNewValidatorSet{value: 2 ether}(1 ether, 1 ether);
}
function _setupValidatorData() internal {
// Build validator points and addresses.
_validatorPoints = new uint128[](10);
_validatorPoints[0] = uint128(1111);
_validatorPoints[1] = uint128(2222);
_validatorPoints[2] = uint128(3333);
_validatorPoints[3] = uint128(4444);
_validatorPoints[4] = uint128(5555);
_validatorPoints[5] = uint128(6666);
_validatorPoints[6] = uint128(7777);
_validatorPoints[7] = uint128(8888);
_validatorPoints[8] = uint128(9999);
_validatorPoints[9] = uint128(101010);
_validatorAddresses = new address[](10);
_validatorAddresses[0] = address(0xFFFF1);
_validatorAddresses[1] = address(0xFFFF2);
_validatorAddresses[2] = address(0xFFFF3);
_validatorAddresses[3] = address(0xFFFF4);
_validatorAddresses[4] = address(0xFFFF5);
_validatorAddresses[5] = address(0xFFFF6);
_validatorAddresses[6] = address(0xFFFF7);
_validatorAddresses[7] = address(0xFFFF8);
_validatorAddresses[8] = address(0xFFFF9);
_validatorAddresses[9] = address(0xFFFFA);
_validatorPointsMerkleRoot =
_buildValidatorPointsMerkleTree(_validatorAddresses, _validatorPoints);
}
function _createRewardsMessage() internal view returns (InboundMessageV2 memory) {
CallContractParams memory updateRewardsCommandParams = CallContractParams({
target: address(rewardsRegistry),
data: abi.encodeWithSelector(
bytes4(keccak256("updateRewardsMerkleRoot(bytes32)")), _validatorPointsMerkleRoot
),
value: 0
});
CommandV2 memory updateRewardsCommand = CommandV2({
kind: CommandKind.CallContract,
gas: 1000000,
payload: abi.encode(updateRewardsCommandParams)
});
CommandV2[] memory commands = new CommandV2[](1);
commands[0] = updateRewardsCommand;
return InboundMessageV2({
origin: REWARDS_MESSAGE_ORIGIN, nonce: 0, topic: bytes32(0), commands: commands
});
}
function _buildMessagesProofForGoodRewardsMessage(
InboundMessageV2 memory updateRewardsMessage
) internal pure returns (bytes32[] memory) {
InboundMessageV2[] memory messages = new InboundMessageV2[](3);
// The first message is the actual rewards message that we want to submit and then claim.
messages[0] = updateRewardsMessage;
// The second message is a dummy message with a different origin.
messages[1] = InboundMessageV2({
origin: WRONG_MESSAGE_ORIGIN, nonce: 1, topic: bytes32(0), commands: new CommandV2[](0)
});
// The third message is an attempt at setting the new rewards root, but with a wrong origin
// i.e. not the origin of the authorised Agent.
messages[2] = InboundMessageV2({
origin: WRONG_MESSAGE_ORIGIN,
nonce: 2,
topic: bytes32(0),
commands: updateRewardsMessage.commands
});
return _buildMessagesProof(messages, 0);
}
function _buildMessagesProofForBadRewardsMessage(
InboundMessageV2 memory goodUpdateRewardsMessage
) internal pure returns (InboundMessageV2 memory, bytes32[] memory) {
InboundMessageV2[] memory messages = new InboundMessageV2[](3);
// The first message is the actual rewards message that we want to submit and then claim.
messages[0] = goodUpdateRewardsMessage;
// The second message is a dummy message with a different origin.
messages[1] = InboundMessageV2({
origin: WRONG_MESSAGE_ORIGIN, nonce: 1, topic: bytes32(0), commands: new CommandV2[](0)
});
// The third message is an attempt at setting the new rewards root, but with a wrong origin
// i.e. not the origin of the authorised Agent.
messages[2] = InboundMessageV2({
origin: WRONG_MESSAGE_ORIGIN,
nonce: 2,
topic: bytes32(0),
commands: goodUpdateRewardsMessage.commands
});
return (messages[2], _buildMessagesProof(messages, 2));
}
function _createBeefyProof() internal pure returns (BeefyVerification.Proof memory) {
// Build BEEFY partial leaf.
BeefyVerification.MMRLeafPartial memory partialLeaf = BeefyVerification.MMRLeafPartial({
version: 0,
parentNumber: 18122022,
parentHash: keccak256(abi.encode(18122022)),
nextAuthoritySetID: 18122022,
nextAuthoritySetLen: 10,
nextAuthoritySetRoot: keccak256(abi.encode(18122022))
});
// Build BEEFY proof.
// Any non-empty BEEFY proof will do for the mock.
bytes32[] memory proof = new bytes32[](1);
proof[0] = keccak256(abi.encode(18122022));
return
BeefyVerification.Proof({leafPartial: partialLeaf, leafProof: proof, leafProofOrder: 0});
}
function _mockBeefyVerification() internal {
// Mock the BeefyVerification.verifyBeefyMMRLeaf to always return true
bytes memory encodedReturn = abi.encode(true);
// Create the function selector for verifyBeefyMMRLeaf
bytes4 selector = BeefyClient.verifyMMRLeafProof.selector;
// Mock any call to this function with any parameters to return true
vm.mockCall(address(beefyClient), abi.encodeWithSelector(selector), encodedReturn);
}
function _buildValidatorPointsMerkleTree(
address[] memory validators,
uint128[] memory points
) internal pure returns (bytes32) {
require(
validators.length == points.length,
"Validators and points arrays must be of the same length"
);
bytes32[] memory leaves = new bytes32[](validators.length);
for (uint256 i = 0; i < validators.length; i++) {
// Use SCALE encoding for Substrate compatibility
bytes memory preimage =
abi.encodePacked(validators[i], ScaleCodec.encodeU32(uint32(points[i])));
leaves[i] = keccak256(preimage);
}
// We calculate the merkle root without sorting for Substrate positional merkle tree.
return MerkleUtils.calculateMerkleRoot(leaves, false);
}
function _buildValidatorPointsProof(
address[] memory validators,
uint128[] memory points,
uint256 leafIndex
) internal pure returns (bytes32[] memory) {
require(
validators.length == points.length,
"Validators and points arrays must be of the same length"
);
bytes32[] memory leaves = new bytes32[](validators.length);
for (uint256 i = 0; i < validators.length; i++) {
// Use SCALE encoding for Substrate compatibility
bytes memory preimage =
abi.encodePacked(validators[i], ScaleCodec.encodeU32(uint32(points[i])));
leaves[i] = keccak256(preimage);
}
return MerkleUtils.buildMerkleProof(leaves, leafIndex, false);
}
function _buildMessagesMerkleTree(
InboundMessageV2[] memory messages
) internal pure returns (bytes32) {
bytes32[] memory leaves = new bytes32[](messages.length);
for (uint256 i = 0; i < messages.length; i++) {
leaves[i] = keccak256(abi.encode(messages[i]));
}
// We calculate the merkle root by sorting the pair before hashing (See Open Zeppelin Merkle Tree lib).
return MerkleUtils.calculateMerkleRoot(leaves, true);
}
function _buildMessagesProof(
InboundMessageV2[] memory messages,
uint256 leafIndex
) internal pure returns (bytes32[] memory) {
bytes32[] memory leaves = new bytes32[](messages.length);
for (uint256 i = 0; i < messages.length; i++) {
leaves[i] = keccak256(abi.encode(messages[i]));
}
return MerkleUtils.buildMerkleProof(leaves, leafIndex, true);
}
}