For detailed developer documentation, please visit our GitHub Pages site. This site is autogenerated from the code and provides comprehensive information about the project's structure, functions, and usage.
This project is a proof of concept for a decentralized bridge secured with attestations by EigenLayer's AVS operators. The goal is to demonstrate how EigenLayer can be used to secure cross-chain token transfers without direct communication between chains. Arbitrary message passing allows not only instantaneous settlement but also the ability to transfer between non VM-compatible chains such as Ethereum and Solana, or Bitcoin.
EigenLayer is a protocol that allows operators to receive delegated stake in order to secure various applications (such as applications relying on off-chain state). Operators are incentivized through application rewards and penalized through stake slashing if they act maliciously.
EigenLayer relies on at least one smart contract interacting with the EigenLayer protocol, that registers and tracks operators and their stake, as well as defines the rules for rewards and penalties.
This proof of concept aims to showcase the potential of EigenLayer in securing decentralized bridges. It includes smart contracts that handle bridging operations, attestation verification, and slashing mechanisms.
-
Contracts:
ECDSAUtils.sol
: Provides ECDSA signature utilities.BridgeServiceManager.sol
: Manages bridge operations and attestation validations.PermissionedBridge.sol
: Manages bridge operations with manually set operator weights.Events.sol
: Contains event definitions for bridge operations.Structs.sol
: Defines structs and related functions for bridge operations.Vault.sol
: Abstract contract providing common vault functionality for bridge contracts.
-
Tests:
EigenLayerBridge.t.sol
: Tests forBridgeServiceManager.sol
.PermissionedBridge.t.sol
: Tests forPermissionedBridge.sol
.
-
Clone the repository:
git clone https://github.com/your-repo/eigenlayer-bridge.git cd eigenlayer-bridge
-
Install Foundry:
curl -L https://foundry.paradigm.xyz | bash foundryup
To run the tests, use the Foundry framework. Running the tests requires an HTTP endpoint to Ethereum mainnet.
The tests use an anvil fork-test from latest Ethereum mainnet state in order to simulate a realistic environment and test against the EigenLayer contracts deployed there.
forge test --rpc-url <RPC_URL>
The ECDSAUtils
contract provides utilities for verifying ECDSA signatures. These utilities are essential for ensuring the authenticity and integrity of the bridge request data. The process involves two main functions:
- getDigest: Computes the EIP712 digest of the bridge request data, which is used for signing.
- getSigner: Recovers the signer's address from the given bridge request data and signature.
This function computes the EIP712 digest for the given bridge request data, ensuring that the data can be signed in a standardized and secure way.
function getDigest(Structs.BridgeRequestData memory data) public view returns (bytes32) {
return _hashTypedDataV4(
keccak256(
abi.encode(
keccak256(
"BridgeRequestData(address user,address tokenAddress,uint256 amountIn,uint256 amountOut,address destinationVault,address destinationAddress,uint256 transferIndex)"
),
data.user,
data.tokenAddress,
data.amountIn,
data.amountOut,
data.destinationVault,
data.destinationAddress,
data.transferIndex
)
)
);
}
The attestation mechanism leverages EigenLayer's AVS operators to secure the bridge. Here's a detailed explanation of the process:
- Bridge Request Creation: A user creates a bridge request, which is stored on-chain and emits an event for operators to observe.
- Signature Collection: AVS operators observe the bridge request event and sign the request data using their private keys. These signatures are then submitted back to the bridge contract.
- Attestation Verification: The bridge contract verifies these signatures to ensure they are from valid operators with sufficient stake.
- Fund Release: Once the contract verifies that enough valid signatures (attestations) have been collected, it releases the funds to the destination address.
This function allows an AVS operator to publish an attestation for a bridge request. It verifies the operator's weight and ensures no double attestations for the same request.
function publishAttestation(bytes memory attestation, uint256 _bridgeRequestId) public nonReentrant onlyOperator {
require(operatorHasMinimumWeight(msg.sender), "Operator does not have minimum weight");
require(!operatorResponses[msg.sender][_bridgeRequestId], "Operator has already responded to the task");
require(msg.sender == getSigner(bridgeRequests[_bridgeRequestId], attestation), "Invalid attestation signature");
uint256 operatorWeight = getOperatorWeight(msg.sender);
bridgeRequestWeights[_bridgeRequestId] += operatorWeight;
rewardAttestation(msg.sender);
emit AVSAttestation(attestation, _bridgeRequestId, operatorWeight);
}
The fund release process is critical to ensuring that the bridge operates securely and efficiently. The process involves the following steps:
- Signature Verification: The contract verifies that the submitted signatures are valid and from authorized operators.
- Weight Calculation: It sums the weights of the valid signatures to ensure that the total weight meets or exceeds the required threshold.
- Fund Transfer: If the total weight is sufficient, the contract transfers the funds to the destination address.
- Incentivizing Validators: The contract pays out gas costs and a small incentive to the caller who initiates the fund release, encouraging users to participate in this final step.
This function releases the funds to the destination address once the required attestations are verified. It ensures the transaction's economic security by checking the total attested weight.
function releaseFunds(bytes[] memory signatures, Structs.BridgeRequestData memory data) public nonReentrant {
uint256 totalWeight = 0;
for (uint256 i = 0; i < signatures.length; i++) {
address signer = getSigner(data, signatures[i]);
require(operatorResponses[signer][data.transferIndex], "Invalid signature");
totalWeight += getOperatorWeight(signer);
}
require(totalWeight >= data.amountOut, "Insufficient total weight to cover swap");
IERC20(data.tokenAddress).transfer(data.destinationAddress, data.amountOut);
payoutCrankGasCost();
emit FundsReleased(data.tokenAddress, data.destinationAddress, data.amountOut);
}
EigenLayer's AVS operators play a crucial role in the security of the bridge. These operators:
- Monitor: Observe events emitted by the bridge contract to detect new bridge requests.
- Sign: Validate the bridge request and provide their signature as an attestation.
- Submit: Send their attestations back to the bridge contract.
- Get Rewarded: Receive rewards for valid attestations and get penalized for any malicious activities through slashing.
These interactions ensure that the bridge is both secure and decentralized, relying on the collective security provided by the EigenLayer's staked operators.
This section covers the testing of our smart contracts using the Forge test framework. The tests are written in Solidity and simulate various scenarios to ensure the correctness of the bridge operations and attestation mechanisms.
The test environment is set up using the setUp
function in each test contract. This function initializes the contract instances, allocates initial balances, and configures the necessary parameters.
function setUp() public {
(operator, operatorPrivateKey) = makeAddrAndKey("operator");
localVault = new BridgeServiceManager(
aVSDirectory, stakeRegistry, rewardsCoordinator, delegationManager,
crankGasCost, 0, bridgeFee, "PermissionedBridge", "1"
);
localVault.initialize();
remoteVault = new BridgeServiceManager(
aVSDirectory, stakeRegistry, rewardsCoordinator, delegationManager,
crankGasCost, 0, bridgeFee, "PermissionedBridge", "1"
);
remoteVault.initialize();
deal(bob, 1 ether);
deal(usdc, bob, 1000 * 10**6);
deal(usdc, address(remoteVault), 1000 * 10**6);
deal(operator, 1 ether);
}
This test case ensures that a bridge request emits the correct events when created.
function testBridgeRequestEmitEvents() public returns (Structs.BridgeRequestData memory) {
vm.startPrank(bob);
IERC20(usdc).approve(address(localVault), 1000 * 10**6);
vm.expectEmit(true, true, true, true);
emit Events.BridgeRequest(bob, usdc, 0, 1000 * 10**6, 1000 * 10**6, address(remote
Vault), alice, 0);
localVault.bridge{value: bridgeFee}(usdc, 1000 * 10**6, 1000 * 10**6, address(remoteVault), alice);
vm.stopPrank();
return Structs.BridgeRequestData(
bob,
usdc,
1000 * 10**6,
1000 * 10**6,
address(remoteVault),
alice,
0
);
}
This test case verifies that an operator can submit an attestation for a bridge request.
function testOperatorCanSubmitAttestation() public {
testBridgeRequestEmitEvents();
(
address user,
address tokenAddress,
uint256 amountIn,
uint256 amountOut,
address destinationVault,
address destinationAddress,
uint256 transferIndex
) = localVault.bridgeRequests(0);
Structs.BridgeRequestData memory bridgeRequest = Structs.BridgeRequestData(
user,
tokenAddress,
amountIn,
amountOut,
destinationVault,
destinationAddress,
transferIndex
);
bytes memory attestation = signBridgeRequestData(localVault, bridgeRequest, operatorPrivateKey);
vm.prank(operator);
vm.expectEmit(true, true, true, true);
emit Events.AVSAttestation(attestation, 0, 1000 ether);
localVault.publishAttestation(attestation, 0);
}
This test case ensures that the bridge completes and releases funds correctly.
function testBridgeCompletesReleaseFunds() public {
testBridgeRequestEmitEvents();
(
address user,
address tokenAddress,
uint256 amountIn,
uint256 amountOut,
address destinationVault,
address destinationAddress,
uint256 transferIndex
) = localVault.bridgeRequests(0);
Structs.BridgeRequestData memory bridgeRequest = Structs.BridgeRequestData(
user,
tokenAddress,
amountIn,
amountOut,
destinationVault,
destinationAddress,
transferIndex
);
bytes memory attestation = signBridgeRequestData(remoteVault, bridgeRequest, operatorPrivateKey);
bytes[] memory bridgeRequestSignatures = new bytes[](1);
bridgeRequestSignatures[0] = attestation;
assertEq(IERC20(usdc).balanceOf(address(remoteVault)), 1000 * 10**6);
assertEq(IERC20(usdc).balanceOf(alice), 0);
vm.prank(alice);
remoteVault.releaseFunds(bridgeRequestSignatures, bridgeRequest);
assertEq(IERC20(usdc).balanceOf(address(remoteVault)), 0);
assertEq(IERC20(usdc).balanceOf(alice), 1000 * 10**6);
}
This project is licensed under the UNLICENSED License.
This project is provided "as is" with no guarantees or warranties. It is intended as a proof of concept only and should not be used in production environments. Use at your own risk.