mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-23 17:28:23 +00:00
## 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>
406 lines
16 KiB
Solidity
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);
|
|
}
|
|
}
|