From eebec4f167dbfa8749ada8d03753364230dd7d49 Mon Sep 17 00:00:00 2001 From: Stefan Batalka Date: Mon, 17 Jun 2024 14:09:18 +0100 Subject: [PATCH] Create examples and documentation (#6) * add docs * commit docs to simplify actions and gh-pages publication flow * add gh-pages actions flow --- .github/workflows/gh-pages.yml | 29 ++ .gitignore | 3 - README.md | 332 ++++++++++++++++-- SUMMARY.md | 0 docs/.gitignore | 1 + docs/book.css | 13 + docs/book.toml | 12 + docs/solidity.min.js | 74 ++++ docs/src/README.md | 326 +++++++++++++++++ docs/src/SUMMARY.md | 9 + .../src/ECDSAUtils.sol/contract.ECDSAUtils.md | 69 ++++ .../contract.BridgeServiceManager.md | 240 +++++++++++++ .../contract.EigenLayerBridge.md | 150 ++++++++ docs/src/src/Events.sol/contract.Events.md | 69 ++++ .../contract.PermissionedBridge.md | 189 ++++++++++ docs/src/src/README.md | 9 + docs/src/src/Structs.sol/library.Structs.md | 57 +++ docs/src/src/Vault.sol/abstract.Vault.md | 204 +++++++++++ docs/src/src/Vault.sol/contract.Vault.md | 106 ++++++ foundry.toml | 6 + src/ECDSAUtils.sol | 12 + src/EigenLayerBridge.sol | 93 +++-- src/Events.sol | 21 +- src/PermissionedBridge.sol | 63 ++-- src/Structs.sol | 15 +- src/Vault.sol | 54 ++- test/EigenLayerBridge.t.sol | 13 +- test/PermissionedBridge.t.sol | 21 +- test/tests.md | 139 ++++++++ 29 files changed, 2176 insertions(+), 153 deletions(-) create mode 100644 .github/workflows/gh-pages.yml create mode 100644 SUMMARY.md create mode 100644 docs/.gitignore create mode 100644 docs/book.css create mode 100644 docs/book.toml create mode 100644 docs/solidity.min.js create mode 100644 docs/src/README.md create mode 100644 docs/src/SUMMARY.md create mode 100644 docs/src/src/ECDSAUtils.sol/contract.ECDSAUtils.md create mode 100644 docs/src/src/EigenLayerBridge.sol/contract.BridgeServiceManager.md create mode 100644 docs/src/src/EigenLayerBridge.sol/contract.EigenLayerBridge.md create mode 100644 docs/src/src/Events.sol/contract.Events.md create mode 100644 docs/src/src/PermissionedBridge.sol/contract.PermissionedBridge.md create mode 100644 docs/src/src/README.md create mode 100644 docs/src/src/Structs.sol/library.Structs.md create mode 100644 docs/src/src/Vault.sol/abstract.Vault.md create mode 100644 docs/src/src/Vault.sol/contract.Vault.md create mode 100644 test/tests.md diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml new file mode 100644 index 0000000..5944e74 --- /dev/null +++ b/.github/workflows/gh-pages.yml @@ -0,0 +1,29 @@ +name: github pages + +on: + push: + branches: + - main + pull_request: + +jobs: + deploy: + runs-on: ubuntu-20.04 + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + steps: + - uses: actions/checkout@v2 + + - name: Setup mdBook + uses: peaceiris/actions-mdbook@v2 + with: + mdbook-version: 'latest' + + - run: mdbook build docs + + - name: Deploy + uses: peaceiris/actions-gh-pages@v3 + if: ${{ github.ref == 'refs/heads/main' }} + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./docs/book diff --git a/.gitignore b/.gitignore index 7194a02..069bb37 100644 --- a/.gitignore +++ b/.gitignore @@ -7,9 +7,6 @@ out/ /broadcast/*/31337/ /broadcast/**/dry-run/ -# Docs -docs/ - # Dotenv file .env diff --git a/README.md b/README.md index 9d9edcf..78779c0 100644 --- a/README.md +++ b/README.md @@ -1,66 +1,326 @@ -## Foundry +# Proof of Concept Cross-Chain Bridge Secured by EigenLayer -**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** +![Build Status](https://img.shields.io/badge/build-passing-brightgreen.svg) +![License](https://img.shields.io/badge/license-UNLICENSED-blue.svg) -Foundry consists of: +## Overview -- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). -- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. -- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. -- **Chisel**: Fast, utilitarian, and verbose solidity REPL. +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. -## Documentation +### What is EigenLayer? -https://book.getfoundry.sh/ +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. -## Usage +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. -### Build +### Objective -```shell -$ forge build +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. + +## Project Structure + +- **Contracts**: + - `ECDSAUtils.sol`: Provides ECDSA signature utilities. + - `EigenLayerBridge.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 for `EigenLayerBridge.sol`. + - `PermissionedBridge.t.sol`: Tests for `PermissionedBridge.sol`. + +## Getting Started + +### Prerequisites + +- [Foundry](https://getfoundry.sh/) + +### Installation + +1. **Clone the repository**: + ```bash + git clone https://github.com/your-repo/eigenlayer-bridge.git + cd eigenlayer-bridge + ``` + +2. **Install Foundry**: + ```bash + curl -L https://foundry.paradigm.xyz | bash + foundryup + ``` + +### Running Tests + +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. + +```bash +forge test --rpc-url ``` -### Test +## Detailed Explanations + +### ECDSA Signature Verification + +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: + +1. **getDigest**: Computes the EIP712 digest of the bridge request data, which is used for signing. +2. **getSigner**: Recovers the signer's address from the given bridge request data and signature. + +#### Example Function: `getDigest` + +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. -```shell -$ forge test +```solidity +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 + ) + ) + ); +} ``` -### Format +### Attestation Mechanism -```shell -$ forge fmt +The attestation mechanism leverages EigenLayer's AVS operators to secure the bridge. Here's a detailed explanation of the process: + +1. **Bridge Request Creation**: A user creates a bridge request, which is stored on-chain and emits an event for operators to observe. +2. **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. +3. **Attestation Verification**: The bridge contract verifies these signatures to ensure they are from valid operators with sufficient stake. +4. **Fund Release**: Once the contract verifies that enough valid signatures (attestations) have been collected, it releases the funds to the destination address. + +#### Example Function: `publishAttestation` + +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. + +```solidity +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); +} ``` -### Gas Snapshots +### Fund Release Process + +The fund release process is critical to ensuring that the bridge operates securely and efficiently. The process involves the following steps: + +1. **Signature Verification**: The contract verifies that the submitted signatures are valid and from authorized operators. +2. **Weight Calculation**: It sums the weights of the valid signatures to ensure that the total weight meets or exceeds the required threshold. +3. **Fund Transfer**: If the total weight is sufficient, the contract transfers the funds to the destination address. +4. **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. -```shell -$ forge snapshot +#### Example Function: `releaseFunds` + +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. + +```solidity +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); +} ``` -### Anvil +### AVS Interactions + +EigenLayer's AVS operators play a crucial role in the security of the bridge. These operators: + +1. **Monitor**: Observe events emitted by the bridge contract to detect new bridge requests. +2. **Sign**: Validate the bridge request and provide their signature as an attestation. +3. **Submit**: Send their attestations back to the bridge contract. +4. **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. -```shell -$ anvil +## Testing + +### Introduction + +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. + +### Setting Up the Test Environment + +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. + +#### Example Setup Function + +```solidity +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); +} ``` -### Deploy +### Test Cases + +#### Test Case 1: Bridge Request Emission + +This test case ensures that a bridge request emits the correct events when created. + +```solidity +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(); -```shell -$ forge script script/EigenLayerBridge.sol.s.sol:CounterScript --rpc-url --private-key + return Structs.BridgeRequestData( + bob, + usdc, + 1000 * 10**6, + 1000 * 10**6, + address(remoteVault), + alice, + 0 + ); +} ``` -### Cast +#### Test Case 2: Operator Attestation Submission -```shell -$ cast +This test case verifies that an operator can submit an attestation for a bridge request. + +```solidity +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); +} ``` -### Help +#### Test Case 3: Fund Release Completion + +This test case ensures that the bridge completes and releases funds correctly. + +```solidity +function testBridgeCompletesReleaseFunds() public { + testBridgeRequestEmitEvents(); + + ( + address user, + address tokenAddress, + uint256 amountIn, + uint256 amountOut, + address destinationVault, + address destinationAddress, + uint256 transferIndex + ) = localVault.bridgeRequests(0); -```shell -$ forge --help -$ anvil --help -$ cast --help + 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); +} ``` + +## License + +This project is licensed under the UNLICENSED License. + +## Disclaimer + +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. \ No newline at end of file diff --git a/SUMMARY.md b/SUMMARY.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..4e42a1b --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1 @@ +book/ \ No newline at end of file diff --git a/docs/book.css b/docs/book.css new file mode 100644 index 0000000..b5ce903 --- /dev/null +++ b/docs/book.css @@ -0,0 +1,13 @@ +table { + margin: 0 auto; + border-collapse: collapse; + width: 100%; +} + +table td:first-child { + width: 15%; +} + +table td:nth-child(2) { + width: 25%; +} \ No newline at end of file diff --git a/docs/book.toml b/docs/book.toml new file mode 100644 index 0000000..64a4bce --- /dev/null +++ b/docs/book.toml @@ -0,0 +1,12 @@ +[book] +src = "src" +title = "Eigen-Bridge" + +[output.html] +no-section-label = true +additional-js = ["solidity.min.js"] +additional-css = ["book.css"] +git-repository-url = "https://github.com/idatsy/eigen-bridge" + +[output.html.fold] +enable = true diff --git a/docs/solidity.min.js b/docs/solidity.min.js new file mode 100644 index 0000000..1924932 --- /dev/null +++ b/docs/solidity.min.js @@ -0,0 +1,74 @@ +hljs.registerLanguage("solidity",(()=>{"use strict";function e(){try{return!0 +}catch(e){return!1}} +var a=/-?(\b0[xX]([a-fA-F0-9]_?)*[a-fA-F0-9]|(\b[1-9](_?\d)*(\.((\d_?)*\d)?)?|\.\d(_?\d)*)([eE][-+]?\d(_?\d)*)?|\b0)(?!\w|\$)/ +;e()&&(a=a.source.replace(/\\b/g,"(?{ +var a=r(e),o=l(e),c=/[A-Za-z_$][A-Za-z_$0-9.]*/,d=e.inherit(e.TITLE_MODE,{ +begin:/[A-Za-z$_][0-9A-Za-z$_]*/,lexemes:c,keywords:n}),u={className:"params", +begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,lexemes:c,keywords:n, +contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,a,o,s]},_={ +className:"operator",begin:/:=|->/};return{keywords:n,lexemes:c, +contains:[a,o,i,t,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,s,_,{ +className:"function",lexemes:c,beginKeywords:"function",end:"{",excludeEnd:!0, +contains:[d,u,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,_]}]}}, +solAposStringMode:r,solQuoteStringMode:l,HEX_APOS_STRING_MODE:i, +HEX_QUOTE_STRING_MODE:t,SOL_NUMBER:s,isNegativeLookbehindAvailable:e} +;const{baseAssembly:c,solAposStringMode:d,solQuoteStringMode:u,HEX_APOS_STRING_MODE:_,HEX_QUOTE_STRING_MODE:m,SOL_NUMBER:b,isNegativeLookbehindAvailable:E}=o +;return e=>{for(var a=d(e),s=u(e),n=[],i=0;i<32;i++)n[i]=i+1 +;var t=n.map((e=>8*e)),r=[];for(i=0;i<=80;i++)r[i]=i +;var l=n.map((e=>"bytes"+e)).join(" ")+" ",o=t.map((e=>"uint"+e)).join(" ")+" ",g=t.map((e=>"int"+e)).join(" ")+" ",M=[].concat.apply([],t.map((e=>r.map((a=>e+"x"+a))))),p={ +keyword:"var bool string int uint "+g+o+"byte bytes "+l+"fixed ufixed "+M.map((e=>"fixed"+e)).join(" ")+" "+M.map((e=>"ufixed"+e)).join(" ")+" enum struct mapping address new delete if else for while continue break return throw emit try catch revert unchecked _ function modifier event constructor fallback receive error virtual override constant immutable anonymous indexed storage memory calldata external public internal payable pure view private returns import from as using pragma contract interface library is abstract type assembly", +literal:"true false wei gwei szabo finney ether seconds minutes hours days weeks years", +built_in:"self this super selfdestruct suicide now msg block tx abi blockhash gasleft assert require Error Panic sha3 sha256 keccak256 ripemd160 ecrecover addmod mulmod log0 log1 log2 log3 log4" +},O={className:"operator",begin:/[+\-!~*\/%<>&^|=]/ +},C=/[A-Za-z_$][A-Za-z_$0-9]*/,N={className:"params",begin:/\(/,end:/\)/, +excludeBegin:!0,excludeEnd:!0,lexemes:C,keywords:p, +contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,a,s,b,"self"]},f={ +begin:/\.\s*/,end:/[^A-Za-z0-9$_\.]/,excludeBegin:!0,excludeEnd:!0,keywords:{ +built_in:"gas value selector address length push pop send transfer call callcode delegatecall staticcall balance code codehash wrap unwrap name creationCode runtimeCode interfaceId min max" +},relevance:2},y=e.inherit(e.TITLE_MODE,{begin:/[A-Za-z$_][0-9A-Za-z$_]*/, +lexemes:C,keywords:p}),w={className:"built_in", +begin:(E()?"(? The tests use an anvil fork-test from latest Ethereum mainnet state in order to simulate a realistic environment. + +```bash +forge test --rpc-url +``` + +## Detailed Explanations + +### ECDSA Signature Verification + +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: + +1. **getDigest**: Computes the EIP712 digest of the bridge request data, which is used for signing. +2. **getSigner**: Recovers the signer's address from the given bridge request data and signature. + +#### Example Function: `getDigest` + +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. + +```solidity +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 + ) + ) + ); +} +``` + +### Attestation Mechanism + +The attestation mechanism leverages EigenLayer's AVS operators to secure the bridge. Here's a detailed explanation of the process: + +1. **Bridge Request Creation**: A user creates a bridge request, which is stored on-chain and emits an event for operators to observe. +2. **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. +3. **Attestation Verification**: The bridge contract verifies these signatures to ensure they are from valid operators with sufficient stake. +4. **Fund Release**: Once the contract verifies that enough valid signatures (attestations) have been collected, it releases the funds to the destination address. + +#### Example Function: `publishAttestation` + +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. + +```solidity +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); +} +``` + +### Fund Release Process + +The fund release process is critical to ensuring that the bridge operates securely and efficiently. The process involves the following steps: + +1. **Signature Verification**: The contract verifies that the submitted signatures are valid and from authorized operators. +2. **Weight Calculation**: It sums the weights of the valid signatures to ensure that the total weight meets or exceeds the required threshold. +3. **Fund Transfer**: If the total weight is sufficient, the contract transfers the funds to the destination address. +4. **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. + +#### Example Function: `releaseFunds` + +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. + +```solidity +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); +} +``` + +### AVS Interactions + +EigenLayer's AVS operators play a crucial role in the security of the bridge. These operators: + +1. **Monitor**: Observe events emitted by the bridge contract to detect new bridge requests. +2. **Sign**: Validate the bridge request and provide their signature as an attestation. +3. **Submit**: Send their attestations back to the bridge contract. +4. **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. + +## Testing + +### Introduction + +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. + +### Setting Up the Test Environment + +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. + +#### Example Setup Function + +```solidity +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); +} +``` + +### Test Cases + +#### Test Case 1: Bridge Request Emission + +This test case ensures that a bridge request emits the correct events when created. + +```solidity +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 + ); +} +``` + +#### Test Case 2: Operator Attestation Submission + +This test case verifies that an operator can submit an attestation for a bridge request. + +```solidity +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); +} +``` + +#### Test Case 3: Fund Release Completion + +This test case ensures that the bridge completes and releases funds correctly. + +```solidity +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); +} +``` + +## License + +This project is licensed under the UNLICENSED License. + +## Disclaimer + +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. \ No newline at end of file diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md new file mode 100644 index 0000000..2104259 --- /dev/null +++ b/docs/src/SUMMARY.md @@ -0,0 +1,9 @@ +# Summary +- [Home](README.md) +# src + - [ECDSAUtils](src/ECDSAUtils.sol/contract.ECDSAUtils.md) + - [BridgeServiceManager](src/EigenLayerBridge.sol/contract.BridgeServiceManager.md) + - [Events](src/Events.sol/contract.Events.md) + - [PermissionedBridge](src/PermissionedBridge.sol/contract.PermissionedBridge.md) + - [Structs](src/Structs.sol/library.Structs.md) + - [Vault](src/Vault.sol/abstract.Vault.md) diff --git a/docs/src/src/ECDSAUtils.sol/contract.ECDSAUtils.md b/docs/src/src/ECDSAUtils.sol/contract.ECDSAUtils.md new file mode 100644 index 0000000..1d759b3 --- /dev/null +++ b/docs/src/src/ECDSAUtils.sol/contract.ECDSAUtils.md @@ -0,0 +1,69 @@ +# ECDSAUtils +[Git Source](https://github.com/idatsy/eigen-bridge/blob/c580a263608f0a9abe800c41a2d4bf408db0805d/src/ECDSAUtils.sol) + +**Inherits:** +EIP712 + +Provides ECDSA signature utilities for verifying bridge request data + + +## Functions +### constructor + +Initializes the EIP712 domain with the given name and version + + +```solidity +constructor(string memory name, string memory version) EIP712(name, version); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`name`|`string`|The user-readable name of the signing domain| +|`version`|`string`|The current major version of the signing domain| + + +### getDigest + +Computes the EIP712 digest for the given bridge request data + + +```solidity +function getDigest(Structs.BridgeRequestData memory data) public view returns (bytes32); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`data`|`Structs.BridgeRequestData`|The bridge request data to be hashed| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`bytes32`|The EIP712 hash of the given bridge request data| + + +### getSigner + +Recovers the signer address from the given bridge request data and signature + + +```solidity +function getSigner(Structs.BridgeRequestData memory data, bytes memory signature) public view returns (address); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`data`|`Structs.BridgeRequestData`|The bridge request data that was signed| +|`signature`|`bytes`|The ECDSA signature| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`address`|The address of the signer| + + diff --git a/docs/src/src/EigenLayerBridge.sol/contract.BridgeServiceManager.md b/docs/src/src/EigenLayerBridge.sol/contract.BridgeServiceManager.md new file mode 100644 index 0000000..5a51a37 --- /dev/null +++ b/docs/src/src/EigenLayerBridge.sol/contract.BridgeServiceManager.md @@ -0,0 +1,240 @@ +# BridgeServiceManager +[Git Source](https://github.com/idatsy/eigen-bridge/blob/c580a263608f0a9abe800c41a2d4bf408db0805d/src/EigenLayerBridge.sol) + +**Inherits:** +ECDSAServiceManagerBase, [Vault](/src/Vault.sol/abstract.Vault.md) + +Manages bridge operations and attestation validations + +*Extends ECDSAServiceManagerBase and Vault for bridging and staking functionality* + + +## State Variables +### operatorResponses +Tracks bridge requests that this operator has responded to once to avoid duplications + +*Double attestations would technically be valid and allow operators to recursively call until funds are released* + + +```solidity +mapping(address => mapping(uint256 => bool)) public operatorResponses; +``` + + +### bridgeRequestWeights +Tracks the total operator weight attested to a bridge request + +*Helpful for determining when enough attestations have been collected to release funds.* + + +```solidity +mapping(uint256 => uint256) public bridgeRequestWeights; +``` + + +## Functions +### constructor + +Initializes the contract with the necessary addresses and parameters + + +```solidity +constructor( + address _avsDirectory, + address _stakeRegistry, + address _rewardsCoordinator, + address _delegationManager, + uint256 _crankGasCost, + uint256 _AVSReward, + uint256 _bridgeFee, + string memory _name, + string memory _version +) + ECDSAServiceManagerBase(_avsDirectory, _stakeRegistry, _rewardsCoordinator, _delegationManager) + Vault(_crankGasCost, _AVSReward, _bridgeFee, _name, _version); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`_avsDirectory`|`address`|The address of the AVS directory contract, managing AVS-related data for registered operators| +|`_stakeRegistry`|`address`|The address of the stake registry contract, managing registration and stake recording| +|`_rewardsCoordinator`|`address`|The address of the rewards coordinator contract, handling rewards distributions| +|`_delegationManager`|`address`|The address of the delegation manager contract, managing staker delegations to operators| +|`_crankGasCost`|`uint256`|The estimated gas cost for calling release funds, used to calculate rebate and incentivize users to call| +|`_AVSReward`|`uint256`|The total reward for AVS attestation| +|`_bridgeFee`|`uint256`|The total fee charged to the user for bridging| +|`_name`|`string`|The name of the contract, used for EIP-712 domain construction| +|`_version`|`string`|The version of the contract, used for EIP-712 domain construction| + + +### onlyOperator + +Ensures that only registered operators can call the function + + +```solidity +modifier onlyOperator(); +``` + +### rewardAttestation + +Rewards the operator for providing a valid attestation + +*Placeholder for actual AVS reward distribution pending Eigen M2 implementation* + + +```solidity +function rewardAttestation(address operator) internal; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`operator`|`address`|The address of the operator to be rewarded| + + +### publishAttestation + +Publishes an attestation for a bridge request + + +```solidity +function publishAttestation(bytes memory attestation, uint256 _bridgeRequestId) public nonReentrant onlyOperator; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`attestation`|`bytes`|The signed attestation| +|`_bridgeRequestId`|`uint256`|The ID of the bridge request| + + +### slashMaliciousAttestor + +Slashes a malicious attestor's stake + +*Placeholder for slashing logic pending Eigen implementations* + + +```solidity +function slashMaliciousAttestor(address operator, uint256 penalty) internal; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`operator`|`address`|The address of the operator to be slashed| +|`penalty`|`uint256`|The penalty amount to be slashed| + + +### challengeAttestation + +Challenges a potentially fraudulent attestation + + +```solidity +function challengeAttestation( + bytes memory fraudulentSignature, + Structs.BridgeRequestData memory fraudulentBridgeRequest +) public nonReentrant; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`fraudulentSignature`|`bytes`|The signature of the fraudulent attestation| +|`fraudulentBridgeRequest`|`Structs.BridgeRequestData`|The data of the fraudulent bridge request| + + +### payoutCrankGasCost + +Payouts the crank gas cost to the caller + + +```solidity +function payoutCrankGasCost() internal; +``` + +### _releaseFunds + +Releases funds to the destination address + + +```solidity +function _releaseFunds(bytes memory data) public override nonReentrant; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`data`|`bytes`|The bridge request data and signatures| + + +### releaseFunds + +Releases funds to the destination address with typed data for ABI construction + + +```solidity +function releaseFunds(bytes[] memory signatures, Structs.BridgeRequestData memory data) public nonReentrant; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`signatures`|`bytes[]`|The signatures of the operators attesting to the bridge request| +|`data`|`Structs.BridgeRequestData`|The bridge request data| + + +### operatorHasMinimumWeight + +Checks if the operator has the minimum required weight + + +```solidity +function operatorHasMinimumWeight(address operator) public view returns (bool); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`operator`|`address`|The address of the operator| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`bool`|True if the operator has the minimum weight, false otherwise| + + +### getOperatorWeight + +Gets the weight of an operator + + +```solidity +function getOperatorWeight(address operator) public view returns (uint256); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`operator`|`address`|The address of the operator| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|The weight of the operator| + + +### receive + +Fallback function to receive ether + + +```solidity +receive() external payable; +``` + diff --git a/docs/src/src/EigenLayerBridge.sol/contract.EigenLayerBridge.md b/docs/src/src/EigenLayerBridge.sol/contract.EigenLayerBridge.md new file mode 100644 index 0000000..5e55a89 --- /dev/null +++ b/docs/src/src/EigenLayerBridge.sol/contract.EigenLayerBridge.md @@ -0,0 +1,150 @@ +# EigenLayerBridge +[Git Source](https://github.com/idatsy/eigen-bridge/blob/4bbab8924ec1c5205dc848c3b60057e0c417dbf1/src/EigenLayerBridge.sol) + +**Inherits:** +ECDSAServiceManagerBase, [Vault](/src/Vault.sol/abstract.Vault.md) + + +## State Variables +### operatorResponses +Tracks bridge requests that this operator has responded to once to avoid duplications + +*Double attestations would technically be valid and allow operators to recursively call until funds are released* + + +```solidity +mapping(address => mapping(uint256 => bool)) public operatorResponses; +``` + + +### bridgeRequestWeights +Tracks the total operator weight attested to a bridge request + +*Helpful for determining when enough attestations have been collected to release funds.* + + +```solidity +mapping(uint256 => uint256) public bridgeRequestWeights; +``` + + +## Functions +### constructor + +*Constructor for ECDSAServiceManagerBase, initializing immutable contract addresses and disabling initializers.* + + +```solidity +constructor( + address _avsDirectory, + address _stakeRegistry, + address _rewardsCoordinator, + address _delegationManager, + uint256 _crankGasCost, + uint256 _AVSReward, + uint256 _bridgeFee, + string memory _name, + string memory _version +) + ECDSAServiceManagerBase(_avsDirectory, _stakeRegistry, _rewardsCoordinator, _delegationManager) + Vault(_crankGasCost, _AVSReward, _bridgeFee, _name, _version); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`_avsDirectory`|`address`|The address of the AVS directory contract, managing AVS-related data for registered operators.| +|`_stakeRegistry`|`address`|The address of the stake registry contract, managing registration and stake recording.| +|`_rewardsCoordinator`|`address`|The address of the rewards coordinator contract, handling rewards distributions.| +|`_delegationManager`|`address`|The address of the delegation manager contract, managing staker delegations to operators.| +|`_crankGasCost`|`uint256`|The estimated gas cost for calling release funds, used to calculate rebate and incentivise users to call.| +|`_AVSReward`|`uint256`|The total reward for AVS attestation.| +|`_bridgeFee`|`uint256`|The total fee charged to user for bridging.| +|`_name`|`string`|The name of the contract. Used for EIP-712 domain construction.| +|`_version`|`string`|The version of the contract. Used for EIP-712 domain construction.| + + +### onlyOperator + + +```solidity +modifier onlyOperator(); +``` + +### rewardAttestation + + +```solidity +function rewardAttestation(address operator) internal; +``` + +### publishAttestation + + +```solidity +function publishAttestation(bytes memory attestation, uint256 _bridgeRequestId) public nonReentrant onlyOperator; +``` + +### slashMaliciousAttestor + + +```solidity +function slashMaliciousAttestor(address operator, uint256 penalty) internal; +``` + +### challengeAttestation + + +```solidity +function challengeAttestation( + bytes memory fraudulentSignature, + Structs.BridgeRequestData memory fraudulentBridgeRequest +) public nonReentrant; +``` + +### payoutCrankGasCost + + +```solidity +function payoutCrankGasCost() internal; +``` + +### _releaseFunds + +Release funds to the destination address + + +```solidity +function _releaseFunds(bytes memory data) public override nonReentrant; +``` + +### releaseFunds + +*Convenience function for releasing funds to the destination address with typed data for ABI construction* + + +```solidity +function releaseFunds(bytes[] memory signatures, Structs.BridgeRequestData memory data) public nonReentrant; +``` + +### operatorHasMinimumWeight + + +```solidity +function operatorHasMinimumWeight(address operator) public view returns (bool); +``` + +### getOperatorWeight + + +```solidity +function getOperatorWeight(address operator) public view returns (uint256); +``` + +### receive + + +```solidity +receive() external payable; +``` + diff --git a/docs/src/src/Events.sol/contract.Events.md b/docs/src/src/Events.sol/contract.Events.md new file mode 100644 index 0000000..9e9f4eb --- /dev/null +++ b/docs/src/src/Events.sol/contract.Events.md @@ -0,0 +1,69 @@ +# Events +[Git Source](https://github.com/idatsy/eigen-bridge/blob/c580a263608f0a9abe800c41a2d4bf408db0805d/src/Events.sol) + +Contains event definitions for bridge operations + + +## Events +### BridgeRequest +Emitted when a new bridge request is created + + +```solidity +event BridgeRequest( + address indexed user, + address indexed tokenAddress, + uint256 indexed bridgeRequestId, + uint256 amountIn, + uint256 amountOut, + address destinationVault, + address destinationAddress, + uint256 transferIndex +); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`user`|`address`|The address of the user initiating the bridge request| +|`tokenAddress`|`address`|The address of the token to be bridged| +|`bridgeRequestId`|`uint256`|The unique ID of the bridge request| +|`amountIn`|`uint256`|The amount of tokens to be bridged| +|`amountOut`|`uint256`|The amount of tokens expected at the destination| +|`destinationVault`|`address`|The address of the destination vault| +|`destinationAddress`|`address`|The address of the recipient at the destination| +|`transferIndex`|`uint256`|The transfer index for unique tracking| + +### AVSAttestation +Emitted when an attestation is published by an AVS operator + + +```solidity +event AVSAttestation(bytes indexed attestation, uint256 indexed bridgeRequestId, uint256 indexed operatorWeight); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`attestation`|`bytes`|The attestation data| +|`bridgeRequestId`|`uint256`|The ID of the bridge request| +|`operatorWeight`|`uint256`|The weight of the operator attesting| + +### FundsReleased +Emitted when funds are released to the destination address + + +```solidity +event FundsReleased(address indexed destinationVault, address indexed destinationAddress, uint256 indexed amountOut); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`destinationVault`|`address`|The address of the destination vault| +|`destinationAddress`|`address`|The address of the recipient at the destination| +|`amountOut`|`uint256`|The amount of tokens released| + diff --git a/docs/src/src/PermissionedBridge.sol/contract.PermissionedBridge.md b/docs/src/src/PermissionedBridge.sol/contract.PermissionedBridge.md new file mode 100644 index 0000000..1d49d7b --- /dev/null +++ b/docs/src/src/PermissionedBridge.sol/contract.PermissionedBridge.md @@ -0,0 +1,189 @@ +# PermissionedBridge +[Git Source](https://github.com/idatsy/eigen-bridge/blob/c580a263608f0a9abe800c41a2d4bf408db0805d/src/PermissionedBridge.sol) + +**Inherits:** +[Vault](/src/Vault.sol/abstract.Vault.md) + +Manages bridge operations with manually set operator weights + +*Extends Vault for bridging functionality* + + +## State Variables +### operatorResponses +Tracks bridge requests that this operator has responded to once to avoid duplications + +*Double attestations would technically be valid and allow operators to recursively call until funds are released* + + +```solidity +mapping(address => mapping(uint256 => bool)) public operatorResponses; +``` + + +### bridgeRequestWeights +Tracks the total operator weight attested to a bridge request + +*Helpful for determining when enough attestations have been collected to release funds.* + + +```solidity +mapping(uint256 => uint256) public bridgeRequestWeights; +``` + + +### operatorWeights +Maps operator addresses to their respective weights + +*Temporary solution for illustrative purposes on non-mainnet chains* + + +```solidity +mapping(address => uint256) public operatorWeights; +``` + + +## Functions +### constructor + +Initializes the contract with the necessary parameters + + +```solidity +constructor(uint256 _crankGasCost, uint256 _AVSReward, uint256 _bridgeFee, string memory _name, string memory _version) + Vault(_crankGasCost, _AVSReward, _bridgeFee, _name, _version); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`_crankGasCost`|`uint256`|The estimated gas cost for calling release funds, used to calculate rebate and incentivize users to call| +|`_AVSReward`|`uint256`|The total reward for AVS attestation| +|`_bridgeFee`|`uint256`|The total fee charged to the user for bridging| +|`_name`|`string`|The name of the contract, used for EIP-712 domain construction| +|`_version`|`string`|The version of the contract, used for EIP-712 domain construction| + + +### onlyOperator + +Ensures that only operators with non-zero weight can call the function + + +```solidity +modifier onlyOperator(); +``` + +### publishAttestation + +Publishes an attestation for a bridge request + + +```solidity +function publishAttestation(bytes memory attestation, uint256 _bridgeRequestId) public nonReentrant onlyOperator; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`attestation`|`bytes`|The signed attestation| +|`_bridgeRequestId`|`uint256`|The ID of the bridge request| + + +### _releaseFunds + +Releases funds to the destination address + + +```solidity +function _releaseFunds(bytes memory data) public override nonReentrant; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`data`|`bytes`|The bridge request data and signatures| + + +### releaseFunds + +Releases funds to the destination address with typed data for ABI construction + + +```solidity +function releaseFunds(bytes[] memory signatures, Structs.BridgeRequestData memory data) public nonReentrant; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`signatures`|`bytes[]`|The signatures of the operators attesting to the bridge request| +|`data`|`Structs.BridgeRequestData`|The bridge request data| + + +### operatorHasMinimumWeight + +Checks if the operator has the minimum required weight + + +```solidity +function operatorHasMinimumWeight(address operator) public view returns (bool); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`operator`|`address`|The address of the operator| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`bool`|True if the operator has the minimum weight, false otherwise| + + +### getOperatorWeight + +Gets the weight of an operator + + +```solidity +function getOperatorWeight(address operator) public view returns (uint256); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`operator`|`address`|The address of the operator| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|The weight of the operator| + + +### setOperatorWeight + +Sets the weight of an operator + + +```solidity +function setOperatorWeight(address operator, uint256 weight) public onlyOwner; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`operator`|`address`|The address of the operator| +|`weight`|`uint256`|The new weight of the operator| + + +### receive + +Fallback function to receive ether + + +```solidity +receive() external payable; +``` + diff --git a/docs/src/src/README.md b/docs/src/src/README.md new file mode 100644 index 0000000..a6e113c --- /dev/null +++ b/docs/src/src/README.md @@ -0,0 +1,9 @@ + + +# Contents +- [ECDSAUtils](ECDSAUtils.sol/contract.ECDSAUtils.md) +- [BridgeServiceManager](EigenLayerBridge.sol/contract.BridgeServiceManager.md) +- [Events](Events.sol/contract.Events.md) +- [PermissionedBridge](PermissionedBridge.sol/contract.PermissionedBridge.md) +- [Structs](Structs.sol/library.Structs.md) +- [Vault](Vault.sol/abstract.Vault.md) diff --git a/docs/src/src/Structs.sol/library.Structs.md b/docs/src/src/Structs.sol/library.Structs.md new file mode 100644 index 0000000..409762a --- /dev/null +++ b/docs/src/src/Structs.sol/library.Structs.md @@ -0,0 +1,57 @@ +# Structs +[Git Source](https://github.com/idatsy/eigen-bridge/blob/c580a263608f0a9abe800c41a2d4bf408db0805d/src/Structs.sol) + +Contains struct definitions and related functions for bridge operations + + +## Functions +### hash + +Computes the hash of the bridge request data + + +```solidity +function hash(BridgeRequestData memory data) internal pure returns (bytes32); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`data`|`BridgeRequestData`|The bridge request data to be hashed| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`bytes32`|The hash of the given bridge request data| + + +## Structs +### BridgeRequestData +Structure representing bridge request data + + +```solidity +struct BridgeRequestData { + address user; + address tokenAddress; + uint256 amountIn; + uint256 amountOut; + address destinationVault; + address destinationAddress; + uint256 transferIndex; +} +``` + +**Properties** + +|Name|Type|Description| +|----|----|-----------| +|`user`|`address`|The address of the user initiating the bridge request| +|`tokenAddress`|`address`|The address of the token to be bridged| +|`amountIn`|`uint256`|The amount of tokens to be bridged| +|`amountOut`|`uint256`|The amount of tokens expected at the destination| +|`destinationVault`|`address`|The address of the destination vault| +|`destinationAddress`|`address`|The address of the recipient at the destination| +|`transferIndex`|`uint256`|The transfer index for unique tracking| + diff --git a/docs/src/src/Vault.sol/abstract.Vault.md b/docs/src/src/Vault.sol/abstract.Vault.md new file mode 100644 index 0000000..216eaaf --- /dev/null +++ b/docs/src/src/Vault.sol/abstract.Vault.md @@ -0,0 +1,204 @@ +# Vault +[Git Source](https://github.com/idatsy/eigen-bridge/blob/c580a263608f0a9abe800c41a2d4bf408db0805d/src/Vault.sol) + +**Inherits:** +[ECDSAUtils](/src/ECDSAUtils.sol/contract.ECDSAUtils.md), [Events](/src/Events.sol/contract.Events.md), ReentrancyGuard, OwnableUpgradeable + +Abstract contract providing common vault functionality for bridge contracts + + +## State Variables +### nextUserTransferIndexes +Stores the transfer index for each user for unique transfer tracking + + +```solidity +mapping(address => uint256) public nextUserTransferIndexes; +``` + + +### currentBridgeRequestId +Global unique bridge request ID + + +```solidity +uint256 public currentBridgeRequestId; +``` + + +### bridgeRequests +Stores history of bridge requests + + +```solidity +mapping(uint256 => Structs.BridgeRequestData) public bridgeRequests; +``` + + +### bridgeFee +Total fee charged to the user for bridging + + +```solidity +uint256 public bridgeFee; +``` + + +### AVSReward +Total reward for AVS attestation + + +```solidity +uint256 public AVSReward; +``` + + +### crankGasCost +Estimated gas cost for calling release funds, used to calculate rebate and incentivize users to call + + +```solidity +uint256 public crankGasCost; +``` + + +### deployer +Address of the contract deployer + + +```solidity +address deployer; +``` + + +## Functions +### constructor + +Initializes the contract with the necessary parameters + + +```solidity +constructor(uint256 _crankGasCost, uint256 _AVSReward, uint256 _bridgeFee, string memory _name, string memory _version) + ECDSAUtils(_name, _version); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`_crankGasCost`|`uint256`|The estimated gas cost for calling release funds, used to calculate rebate and incentivize users to call| +|`_AVSReward`|`uint256`|The total reward for AVS attestation| +|`_bridgeFee`|`uint256`|The total fee charged to the user for bridging| +|`_name`|`string`|The name of the contract, used for EIP-712 domain construction| +|`_version`|`string`|The version of the contract, used for EIP-712 domain construction| + + +### initialize + +Initializes the contract and transfers ownership to the deployer + + +```solidity +function initialize() public initializer; +``` + +### setBridgeFee + +Sets the bridge fee + + +```solidity +function setBridgeFee(uint256 _bridgeFee) external onlyOwner; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`_bridgeFee`|`uint256`|The new bridge fee| + + +### setAVSReward + +Sets the AVS reward + + +```solidity +function setAVSReward(uint256 _AVSReward) external onlyOwner; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`_AVSReward`|`uint256`|The new AVS reward| + + +### setCrankGasCost + +Sets the crank gas cost + + +```solidity +function setCrankGasCost(uint256 _crankGasCost) external onlyOwner; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`_crankGasCost`|`uint256`|The new crank gas cost| + + +### bridgeERC20 + +Internal function to transfer ERC20 tokens for bridging + + +```solidity +function bridgeERC20(address tokenAddress, uint256 amountIn) internal; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`tokenAddress`|`address`|The address of the token to be transferred| +|`amountIn`|`uint256`|The amount of tokens to be transferred| + + +### bridge + +Initiates a bridge request + + +```solidity +function bridge( + address tokenAddress, + uint256 amountIn, + uint256 amountOut, + address destinationVault, + address destinationAddress +) public payable nonReentrant; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`tokenAddress`|`address`|The address of the token to be bridged| +|`amountIn`|`uint256`|The amount of tokens to be bridged| +|`amountOut`|`uint256`|The amount of tokens expected at the destination| +|`destinationVault`|`address`|The address of the destination vault| +|`destinationAddress`|`address`|The address of the recipient at the destination| + + +### _releaseFunds + +Abstract function to release funds, to be implemented by inheriting contracts + + +```solidity +function _releaseFunds(bytes memory data) public virtual; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`data`|`bytes`|The bridge request data and signatures| + + diff --git a/docs/src/src/Vault.sol/contract.Vault.md b/docs/src/src/Vault.sol/contract.Vault.md new file mode 100644 index 0000000..3810d6d --- /dev/null +++ b/docs/src/src/Vault.sol/contract.Vault.md @@ -0,0 +1,106 @@ +# Vault +[Git Source](https://github.com/idatsy/eigen-bridge/blob/ba02380b529b1b58f7d32ebb56870074714e37df/src/Vault.sol) + +**Inherits:** +[ECDSAUtils](/src/ECDSAUtils.sol/contract.ECDSAUtils.md), [Events](/src/Events.sol/contract.Events.md), ReentrancyGuard, OwnableUpgradeable + +*this looks weird, but has to be imported from the same location as any sibling contracts* + + +## State Variables +### nextUserTransferIndexes +Stores the transfer index for each user for unique transfer tracking + +*conveniently solidity mappings start at 0 when uninitialized so we don't have to worry about new users* + + +```solidity +mapping(address => uint256) public nextUserTransferIndexes; +``` + + +### currentBridgeRequestId + +```solidity +uint256 public currentBridgeRequestId; +``` + + +### bridgeRequests + +```solidity +mapping(uint256 => Structs.BridgeRequestData) public bridgeRequests; +``` + + +### bridgeFee + +```solidity +uint256 public bridgeFee; +``` + + +### AVSReward + +```solidity +uint256 public AVSReward; +``` + + +### crankGasCost + +```solidity +uint256 public crankGasCost; +``` + + +## Functions +### constructor + + +```solidity +constructor(uint256 _crankGasCost, uint256 _AVSReward, uint256 _bridgeFee, string memory _name, string memory _version) + ECDSAUtils(_name, _version); +``` + +### setBridgeFee + + +```solidity +function setBridgeFee(uint256 _bridgeFee) external onlyOwner; +``` + +### setAVSReward + + +```solidity +function setAVSReward(uint256 _AVSReward) external onlyOwner; +``` + +### setCrankGasCost + + +```solidity +function setCrankGasCost(uint256 _crankGasCost) external onlyOwner; +``` + +### bridgeERC20 + + +```solidity +function bridgeERC20(address tokenAddress, uint256 amountIn) internal; +``` + +### bridge + + +```solidity +function bridge( + address tokenAddress, + uint256 amountIn, + uint256 amountOut, + address destinationVault, + address destinationAddress +) public payable nonReentrant; +``` + diff --git a/foundry.toml b/foundry.toml index 25b918f..a55eb87 100644 --- a/foundry.toml +++ b/foundry.toml @@ -4,3 +4,9 @@ out = "out" libs = ["lib"] # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options + +[doc] +title = "Eigen-Bridge" +description = "Proof of concept implementation of a cross-chain bridge secured by EigenLayer AVS" +authors = ["Stefan Batalka"] + diff --git a/src/ECDSAUtils.sol b/src/ECDSAUtils.sol index 5e7616b..dc60bd5 100644 --- a/src/ECDSAUtils.sol +++ b/src/ECDSAUtils.sol @@ -6,11 +6,19 @@ import "openzeppelin/contracts/utils/cryptography/EIP712.sol"; import "./Structs.sol"; +/// @title ECDSA Utilities +/// @notice Provides ECDSA signature utilities for verifying bridge request data contract ECDSAUtils is EIP712 { using ECDSA for bytes32; + /// @notice Initializes the EIP712 domain with the given name and version + /// @param name The user-readable name of the signing domain + /// @param version The current major version of the signing domain constructor(string memory name, string memory version) EIP712(name, version) {} + /// @notice Computes the EIP712 digest for the given bridge request data + /// @param data The bridge request data to be hashed + /// @return The EIP712 hash of the given bridge request data function getDigest(Structs.BridgeRequestData memory data) public view returns (bytes32) { return _hashTypedDataV4( keccak256( @@ -30,6 +38,10 @@ contract ECDSAUtils is EIP712 { ); } + /// @notice Recovers the signer address from the given bridge request data and signature + /// @param data The bridge request data that was signed + /// @param signature The ECDSA signature + /// @return The address of the signer function getSigner(Structs.BridgeRequestData memory data, bytes memory signature) public view returns (address) { bytes32 digest = getDigest(data); return ECDSA.recover(digest, signature); diff --git a/src/EigenLayerBridge.sol b/src/EigenLayerBridge.sol index ef3f498..1a7fb35 100644 --- a/src/EigenLayerBridge.sol +++ b/src/EigenLayerBridge.sol @@ -7,31 +7,33 @@ import "openzeppelin/contracts/token/ERC20/IERC20.sol"; import "./Events.sol"; import "./Structs.sol"; import "./ECDSAUtils.sol"; -import "./Vault.sol"; - +import "./Vault.sol"; +/// @title Bridge Service Manager +/// @notice Manages bridge operations and attestation validations +/// @dev Extends ECDSAServiceManagerBase and Vault for bridging and staking functionality contract BridgeServiceManager is ECDSAServiceManagerBase, Vault { using Structs for Structs.BridgeRequestData; /// @notice Tracks bridge requests that this operator has responded to once to avoid duplications /// @dev Double attestations would technically be valid and allow operators to recursively call until funds are released - mapping(address => mapping(uint256=> bool)) public operatorResponses; + mapping(address => mapping(uint256 => bool)) public operatorResponses; /// @notice Tracks the total operator weight attested to a bridge request /// @dev Helpful for determining when enough attestations have been collected to release funds. mapping(uint256 => uint256) public bridgeRequestWeights; /** - * @dev Constructor for ECDSAServiceManagerBase, initializing immutable contract addresses and disabling initializers. - * @param _avsDirectory The address of the AVS directory contract, managing AVS-related data for registered operators. - * @param _stakeRegistry The address of the stake registry contract, managing registration and stake recording. - * @param _rewardsCoordinator The address of the rewards coordinator contract, handling rewards distributions. - * @param _delegationManager The address of the delegation manager contract, managing staker delegations to operators. - * @param _crankGasCost The estimated gas cost for calling release funds, used to calculate rebate and incentivise users to call. - * @param _AVSReward The total reward for AVS attestation. - * @param _bridgeFee The total fee charged to user for bridging. - * @param _name The name of the contract. Used for EIP-712 domain construction. - * @param _version The version of the contract. Used for EIP-712 domain construction. + * @notice Initializes the contract with the necessary addresses and parameters + * @param _avsDirectory The address of the AVS directory contract, managing AVS-related data for registered operators + * @param _stakeRegistry The address of the stake registry contract, managing registration and stake recording + * @param _rewardsCoordinator The address of the rewards coordinator contract, handling rewards distributions + * @param _delegationManager The address of the delegation manager contract, managing staker delegations to operators + * @param _crankGasCost The estimated gas cost for calling release funds, used to calculate rebate and incentivize users to call + * @param _AVSReward The total reward for AVS attestation + * @param _bridgeFee The total fee charged to the user for bridging + * @param _name The name of the contract, used for EIP-712 domain construction + * @param _version The version of the contract, used for EIP-712 domain construction */ constructor( address _avsDirectory, address _stakeRegistry, address _rewardsCoordinator, address _delegationManager, @@ -47,43 +49,37 @@ contract BridgeServiceManager is ECDSAServiceManagerBase, Vault { Vault(_crankGasCost, _AVSReward, _bridgeFee, _name, _version) {} + /// @notice Ensures that only registered operators can call the function modifier onlyOperator() { require( - ECDSAStakeRegistry(stakeRegistry).operatorRegistered(msg.sender) - == - true, + ECDSAStakeRegistry(stakeRegistry).operatorRegistered(msg.sender), "Operator must be the caller" ); _; } - /* AVS functions */ - + /// @notice Rewards the operator for providing a valid attestation + /// @dev Placeholder for actual AVS reward distribution pending Eigen M2 implementation + /// @param operator The address of the operator to be rewarded function rewardAttestation(address operator) internal { - // Calculate payment for AVS attestation, equals the reward or the contract balance uint256 payout = AVSReward; if (address(this).balance < payout) { payout = address(this).balance; } - // TODO: This is a placeholder for the actual AVS reward distribution pending Eigen M2 implementation (bool success, ) = operator.call{value: payout}(""); success; } + /// @notice Publishes an attestation for a bridge request + /// @param attestation The signed attestation + /// @param _bridgeRequestId The ID of the bridge request function publishAttestation(bytes memory attestation, uint256 _bridgeRequestId) public nonReentrant onlyOperator { - // Check minimum weight requirement require(operatorHasMinimumWeight(msg.sender), "Operator does not have minimum weight"); - - // Check that operator doesn't respond to the same task twice require(!operatorResponses[msg.sender][_bridgeRequestId], "Operator has already responded to the task"); - - // Check that the operator is signing the correct bridge request parameters require(msg.sender == getSigner(bridgeRequests[_bridgeRequestId], attestation), "Invalid attestation signature"); - // Increment the total weights attested for this bridge request. - // Helpful for determining when enough attestations have been collected to release funds. uint256 operatorWeight = getOperatorWeight(msg.sender); bridgeRequestWeights[_bridgeRequestId] += operatorWeight; @@ -92,6 +88,10 @@ contract BridgeServiceManager is ECDSAServiceManagerBase, Vault { emit AVSAttestation(attestation, _bridgeRequestId, operatorWeight); } + /// @notice Slashes a malicious attestor's stake + /// @dev Placeholder for slashing logic pending Eigen implementations + /// @param operator The address of the operator to be slashed + /// @param penalty The penalty amount to be slashed function slashMaliciousAttestor(address operator, uint256 penalty) internal { // TODO: Implement slashing logic pending clarity on Eigen implementations // @dev the below code is commented out for the upcoming M2 release @@ -155,29 +155,24 @@ contract BridgeServiceManager is ECDSAServiceManagerBase, Vault { // the task response has been challenged successfully } + /// @notice Challenges a potentially fraudulent attestation + /// @param fraudulentSignature The signature of the fraudulent attestation + /// @param fraudulentBridgeRequest The data of the fraudulent bridge request function challengeAttestation( bytes memory fraudulentSignature, Structs.BridgeRequestData memory fraudulentBridgeRequest ) public nonReentrant { - // Get the signer for this potentially fraudulent bridge request address fraudulentSigner = getSigner(fraudulentBridgeRequest, fraudulentSignature); + require(operatorResponses[fraudulentSigner][fraudulentBridgeRequest.transferIndex], "Operator has not attested to this bridge request"); - // Check that a bridge request exists for this transfer index, and has been signed by the alleged fraudster - require( - operatorResponses[fraudulentSigner][fraudulentBridgeRequest.transferIndex], - "Operator has not attested to this bride request" - ); - - // Check that this signed potentially fraudulent bridge request does not match the actual bridge request - // meaning it would have been manipulated by the operator Structs.BridgeRequestData memory actualBridgeRequest = bridgeRequests[fraudulentBridgeRequest.transferIndex]; if (fraudulentBridgeRequest.hash() != actualBridgeRequest.hash()) { - // Slash the operator for attempting to submit a fraudulent attestation slashMaliciousAttestor(fraudulentSigner, getOperatorWeight(fraudulentSigner)); } } + /// @notice Payouts the crank gas cost to the caller function payoutCrankGasCost() internal { uint256 payout = crankGasCost * tx.gasprice; if (address(this).balance < payout) { @@ -190,14 +185,16 @@ contract BridgeServiceManager is ECDSAServiceManagerBase, Vault { } } - /// @notice Release funds to the destination address + /// @notice Releases funds to the destination address + /// @param data The bridge request data and signatures function _releaseFunds(bytes memory data) public override nonReentrant { releaseFunds(abi.decode(data, (bytes[])), abi.decode(data, (Structs.BridgeRequestData))); } - /// @dev Convenience function for releasing funds to the destination address with typed data for ABI construction + /// @notice Releases funds to the destination address with typed data for ABI construction + /// @param signatures The signatures of the operators attesting to the bridge request + /// @param data The bridge request data function releaseFunds(bytes[] memory signatures, Structs.BridgeRequestData memory data) public nonReentrant { - // Verify each signature and sum the operator weights uint256 totalWeight = 0; for (uint256 i = 0; i < signatures.length; i++) { address signer = getSigner(data, signatures[i]); @@ -205,12 +202,7 @@ contract BridgeServiceManager is ECDSAServiceManagerBase, Vault { totalWeight += getOperatorWeight(signer); } - // Check if the total weight is sufficient to cover the economic value of the swap require(totalWeight >= data.amountOut, "Insufficient total weight to cover swap"); - - // Transfer the tokens to the destination address - // NOTE: This should use an oracle price or a passed down price from original BridgeRequest, for the sake of - // illustrating how EigenLayer works we are using the amountOut as the value of the token here. IERC20(data.tokenAddress).transfer(data.destinationAddress, data.amountOut); payoutCrankGasCost(); @@ -218,15 +210,20 @@ contract BridgeServiceManager is ECDSAServiceManagerBase, Vault { emit FundsReleased(data.tokenAddress, data.destinationAddress, data.amountOut); } - /* Helper functions */ - + /// @notice Checks if the operator has the minimum required weight + /// @param operator The address of the operator + /// @return True if the operator has the minimum weight, false otherwise function operatorHasMinimumWeight(address operator) public view returns (bool) { return ECDSAStakeRegistry(stakeRegistry).getOperatorWeight(operator) >= ECDSAStakeRegistry(stakeRegistry).minimumWeight(); } + /// @notice Gets the weight of an operator + /// @param operator The address of the operator + /// @return The weight of the operator function getOperatorWeight(address operator) public view returns (uint256) { return ECDSAStakeRegistry(stakeRegistry).getOperatorWeight(operator); } + /// @notice Fallback function to receive ether receive() external payable {} -} \ No newline at end of file +} diff --git a/src/Events.sol b/src/Events.sol index aba2518..32bf740 100644 --- a/src/Events.sol +++ b/src/Events.sol @@ -1,7 +1,18 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; +/// @title Event Definitions +/// @notice Contains event definitions for bridge operations contract Events { + /// @notice Emitted when a new bridge request is created + /// @param user The address of the user initiating the bridge request + /// @param tokenAddress The address of the token to be bridged + /// @param bridgeRequestId The unique ID of the bridge request + /// @param amountIn The amount of tokens to be bridged + /// @param amountOut The amount of tokens expected at the destination + /// @param destinationVault The address of the destination vault + /// @param destinationAddress The address of the recipient at the destination + /// @param transferIndex The transfer index for unique tracking event BridgeRequest( address indexed user, address indexed tokenAddress, @@ -13,15 +24,23 @@ contract Events { uint256 transferIndex ); + /// @notice Emitted when an attestation is published by an AVS operator + /// @param attestation The attestation data + /// @param bridgeRequestId The ID of the bridge request + /// @param operatorWeight The weight of the operator attesting event AVSAttestation( bytes indexed attestation, uint256 indexed bridgeRequestId, uint256 indexed operatorWeight ); + /// @notice Emitted when funds are released to the destination address + /// @param destinationVault The address of the destination vault + /// @param destinationAddress The address of the recipient at the destination + /// @param amountOut The amount of tokens released event FundsReleased( address indexed destinationVault, address indexed destinationAddress, uint256 indexed amountOut ); -} \ No newline at end of file +} diff --git a/src/PermissionedBridge.sol b/src/PermissionedBridge.sol index c7578de..4018991 100644 --- a/src/PermissionedBridge.sol +++ b/src/PermissionedBridge.sol @@ -6,31 +6,33 @@ import "openzeppelin/contracts/token/ERC20/IERC20.sol"; import "./Events.sol"; import "./Structs.sol"; import "./ECDSAUtils.sol"; -import "./Vault.sol"; - +import "./Vault.sol"; +/// @title Permissioned Bridge +/// @notice Manages bridge operations with manually set operator weights +/// @dev Extends Vault for bridging functionality contract PermissionedBridge is Vault { using Structs for Structs.BridgeRequestData; /// @notice Tracks bridge requests that this operator has responded to once to avoid duplications /// @dev Double attestations would technically be valid and allow operators to recursively call until funds are released - mapping(address => mapping(uint256=> bool)) public operatorResponses; + mapping(address => mapping(uint256 => bool)) public operatorResponses; /// @notice Tracks the total operator weight attested to a bridge request /// @dev Helpful for determining when enough attestations have been collected to release funds. mapping(uint256 => uint256) public bridgeRequestWeights; - /// @notice This is a hack around the fact that we don't have access to the EigenLayer stake registry on non-mainnet - /// networks. Instead we monitor the operator's weights on mainnet and change them here manually using a watcher. - /// @dev This is a temporary solution for illustrative purposes only. + /// @notice Maps operator addresses to their respective weights + /// @dev Temporary solution for illustrative purposes on non-mainnet chains mapping(address => uint256) public operatorWeights; /** - * @param _crankGasCost The estimated gas cost for calling release funds, used to calculate rebate and incentivise users to call. - * @param _AVSReward The total reward for AVS attestation. - * @param _bridgeFee The total fee charged to user for bridging. - * @param _name The name of the contract. Used for EIP-712 domain construction. - * @param _version The version of the contract. Used for EIP-712 domain construction. + * @notice Initializes the contract with the necessary parameters + * @param _crankGasCost The estimated gas cost for calling release funds, used to calculate rebate and incentivize users to call + * @param _AVSReward The total reward for AVS attestation + * @param _bridgeFee The total fee charged to the user for bridging + * @param _name The name of the contract, used for EIP-712 domain construction + * @param _version The version of the contract, used for EIP-712 domain construction */ constructor( uint256 _crankGasCost, uint256 _AVSReward, uint256 _bridgeFee, @@ -39,67 +41,68 @@ contract PermissionedBridge is Vault { Vault(_crankGasCost, _AVSReward, _bridgeFee, _name, _version) {} + /// @notice Ensures that only operators with non-zero weight can call the function modifier onlyOperator() { require(operatorWeights[msg.sender] > 0, "Operator weight must be greater than 0"); _; } - /* MOCK AVS functions */ - + /// @notice Publishes an attestation for a bridge request + /// @param attestation The signed attestation + /// @param _bridgeRequestId The ID of the bridge request function publishAttestation(bytes memory attestation, uint256 _bridgeRequestId) public nonReentrant onlyOperator { - // Check that operator doesn't respond to the same task twice require(!operatorResponses[msg.sender][_bridgeRequestId], "Operator has already responded to the task"); - - // Check that the operator is signing the correct bridge request parameters require(msg.sender == getSigner(bridgeRequests[_bridgeRequestId], attestation), "Invalid attestation signature"); - // Increment the total weights attested for this bridge request. - // Helpful for determining when enough attestations have been collected to release funds. uint256 operatorWeight = getOperatorWeight(msg.sender); bridgeRequestWeights[_bridgeRequestId] += operatorWeight; emit AVSAttestation(attestation, _bridgeRequestId, operatorWeight); } - /// @notice Release funds to the destination address + /// @notice Releases funds to the destination address + /// @param data The bridge request data and signatures function _releaseFunds(bytes memory data) public override nonReentrant { releaseFunds(abi.decode(data, (bytes[])), abi.decode(data, (Structs.BridgeRequestData))); } - /// @notice Convenience function for releasing funds to the destination address with typed data for ABI construction - /// @dev NOTE: this function is only callable by the owner and always releases the funds to the destination address + /// @notice Releases funds to the destination address with typed data for ABI construction + /// @param signatures The signatures of the operators attesting to the bridge request + /// @param data The bridge request data function releaseFunds(bytes[] memory signatures, Structs.BridgeRequestData memory data) public nonReentrant { - // Verify each signature and sum the operator weights uint256 totalWeight = 0; for (uint256 i = 0; i < signatures.length; i++) { address signer = getSigner(data, signatures[i]); totalWeight += getOperatorWeight(signer); } - // Check if the total weight is sufficient to cover the economic value of the swap require(totalWeight >= data.amountOut, "Insufficient total weight to cover swap"); - - // Transfer the tokens to the destination address - // NOTE: This should use an oracle price or a passed down price from original BridgeRequest, for the sake of - // illustrating how EigenLayer works we are using the amountOut as the value of the token here. IERC20(data.tokenAddress).transfer(data.destinationAddress, data.amountOut); emit FundsReleased(data.tokenAddress, data.destinationAddress, data.amountOut); } - /* Helper functions */ - + /// @notice Checks if the operator has the minimum required weight + /// @param operator The address of the operator + /// @return True if the operator has the minimum weight, false otherwise function operatorHasMinimumWeight(address operator) public view returns (bool) { return operatorWeights[operator] >= 1; } + /// @notice Gets the weight of an operator + /// @param operator The address of the operator + /// @return The weight of the operator function getOperatorWeight(address operator) public view returns (uint256) { return operatorWeights[operator]; } + /// @notice Sets the weight of an operator + /// @param operator The address of the operator + /// @param weight The new weight of the operator function setOperatorWeight(address operator, uint256 weight) public onlyOwner { operatorWeights[operator] = weight; } + /// @notice Fallback function to receive ether receive() external payable {} -} \ No newline at end of file +} diff --git a/src/Structs.sol b/src/Structs.sol index 32d3249..dd6c61d 100644 --- a/src/Structs.sol +++ b/src/Structs.sol @@ -1,7 +1,17 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; +/// @title Struct Definitions +/// @notice Contains struct definitions and related functions for bridge operations library Structs { + /// @notice Structure representing bridge request data + /// @param user The address of the user initiating the bridge request + /// @param tokenAddress The address of the token to be bridged + /// @param amountIn The amount of tokens to be bridged + /// @param amountOut The amount of tokens expected at the destination + /// @param destinationVault The address of the destination vault + /// @param destinationAddress The address of the recipient at the destination + /// @param transferIndex The transfer index for unique tracking struct BridgeRequestData { address user; address tokenAddress; @@ -12,6 +22,9 @@ library Structs { uint256 transferIndex; } + /// @notice Computes the hash of the bridge request data + /// @param data The bridge request data to be hashed + /// @return The hash of the given bridge request data function hash(BridgeRequestData memory data) internal pure returns (bytes32) { return keccak256(abi.encode( data.user, @@ -23,4 +36,4 @@ library Structs { data.transferIndex )); } -} \ No newline at end of file +} diff --git a/src/Vault.sol b/src/Vault.sol index 5dc16fa..3dfe9c2 100644 --- a/src/Vault.sol +++ b/src/Vault.sol @@ -3,31 +3,43 @@ pragma solidity ^0.8.13; import "openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "openzeppelin/contracts/token/ERC20/IERC20.sol"; -// this looks weird, but has to be imported from the same location as any sibling contracts import {OwnableUpgradeable} from "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol"; - import {ECDSAUtils} from "./ECDSAUtils.sol"; import {Structs} from "./Structs.sol"; import {Events} from "./Events.sol"; +/// @title Vault +/// @notice Abstract contract providing common vault functionality for bridge contracts abstract contract Vault is ECDSAUtils, Events, ReentrancyGuard, OwnableUpgradeable { /// @notice Stores the transfer index for each user for unique transfer tracking - /// @dev conveniently solidity mappings start at 0 when uninitialized so we don't have to worry about new users mapping(address => uint256) public nextUserTransferIndexes; - // Global unique bridge request ID + + /// @notice Global unique bridge request ID uint256 public currentBridgeRequestId; - // Stores history of bridge requests + + /// @notice Stores history of bridge requests mapping(uint256 => Structs.BridgeRequestData) public bridgeRequests; - // Total fee charged to user for bridging + /// @notice Total fee charged to the user for bridging uint256 public bridgeFee; - // Total reward for AVS attestation + + /// @notice Total reward for AVS attestation uint256 public AVSReward; - // Estimated gas cost for calling release funds, used to calculate rebate and incentivise users to call + + /// @notice Estimated gas cost for calling release funds, used to calculate rebate and incentivize users to call uint256 public crankGasCost; + /// @notice Address of the contract deployer address deployer; + /** + * @notice Initializes the contract with the necessary parameters + * @param _crankGasCost The estimated gas cost for calling release funds, used to calculate rebate and incentivize users to call + * @param _AVSReward The total reward for AVS attestation + * @param _bridgeFee The total fee charged to the user for bridging + * @param _name The name of the contract, used for EIP-712 domain construction + * @param _version The version of the contract, used for EIP-712 domain construction + */ constructor( uint256 _crankGasCost, uint256 _AVSReward, uint256 _bridgeFee, string memory _name, string memory _version ) ECDSAUtils(_name, _version) { @@ -38,27 +50,33 @@ abstract contract Vault is ECDSAUtils, Events, ReentrancyGuard, OwnableUpgradeab deployer = msg.sender; } + /// @notice Initializes the contract and transfers ownership to the deployer function initialize() public initializer { __Ownable_init(); transferOwnership(deployer); } - /* Access control functions and fee setters */ - + /// @notice Sets the bridge fee + /// @param _bridgeFee The new bridge fee function setBridgeFee(uint256 _bridgeFee) external onlyOwner { bridgeFee = _bridgeFee; } + /// @notice Sets the AVS reward + /// @param _AVSReward The new AVS reward function setAVSReward(uint256 _AVSReward) external onlyOwner { AVSReward = _AVSReward; } + /// @notice Sets the crank gas cost + /// @param _crankGasCost The new crank gas cost function setCrankGasCost(uint256 _crankGasCost) external onlyOwner { crankGasCost = _crankGasCost; } - /* Bridge functions */ - + /// @notice Internal function to transfer ERC20 tokens for bridging + /// @param tokenAddress The address of the token to be transferred + /// @param amountIn The amount of tokens to be transferred function bridgeERC20(address tokenAddress, uint256 amountIn) internal { bool success = IERC20(tokenAddress).transferFrom( msg.sender, @@ -68,6 +86,14 @@ abstract contract Vault is ECDSAUtils, Events, ReentrancyGuard, OwnableUpgradeab require(success, "Transfer failed"); } + /** + * @notice Initiates a bridge request + * @param tokenAddress The address of the token to be bridged + * @param amountIn The amount of tokens to be bridged + * @param amountOut The amount of tokens expected at the destination + * @param destinationVault The address of the destination vault + * @param destinationAddress The address of the recipient at the destination + */ function bridge( address tokenAddress, uint256 amountIn, @@ -105,7 +131,7 @@ abstract contract Vault is ECDSAUtils, Events, ReentrancyGuard, OwnableUpgradeab nextUserTransferIndexes[msg.sender]++; } - /// @notice Fund releasing logic is implementation specific and should be implemented in the inheriting contract - /// in order to ensure that the funds are released correctly + /// @notice Abstract function to release funds, to be implemented by inheriting contracts + /// @param data The bridge request data and signatures function _releaseFunds(bytes memory data) public virtual; } diff --git a/test/EigenLayerBridge.t.sol b/test/EigenLayerBridge.t.sol index 0af2dd8..1a8e672 100644 --- a/test/EigenLayerBridge.t.sol +++ b/test/EigenLayerBridge.t.sol @@ -2,19 +2,18 @@ pragma solidity ^0.8.13; import {Test, console} from "forge-std/Test.sol"; - import {BridgeServiceManager} from "../src/EigenLayerBridge.sol"; import "../src/Events.sol"; import {Structs} from "../src/Structs.sol"; - import "openzeppelin/contracts/utils/cryptography/EIP712.sol"; +/// @title BridgeServiceManager Test +/// @notice This contract tests the functionalities of the BridgeServiceManager contract using Forge test framework contract BridgeServiceManagerTest is Test, EIP712("BridgeServiceManager", "1") { BridgeServiceManager public localVault; BridgeServiceManager public remoteVault; address usdc = address(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); - uint256 bridgeFee = 0.005 ether; uint256 crankGasCost = 100_000; @@ -24,13 +23,12 @@ contract BridgeServiceManagerTest is Test, EIP712("BridgeServiceManager", "1") { uint256 operatorPrivateKey; // MAINNET EIGENLAYER CONTRACTS - // https://github.com/Layr-Labs/eigenlayer-contracts?tab=readme-ov-file#deployments address delegationManager = address(0x39053D51B77DC0d36036Fc1fCc8Cb819df8Ef37A); address aVSDirectory = address(0x39053D51B77DC0d36036Fc1fCc8Cb819df8Ef37A); - ///https://github.com/Layr-Labs/eigenlayer-middleware/tree/mainnet address rewardsCoordinator = address(0x0BAAc79acD45A023E19345c352d8a7a83C4e5656); address stakeRegistry = address(0x006124Ae7976137266feeBFb3F4D2BE4C073139D); + /// @notice Sets up the test environment function setUp() public { (operator, operatorPrivateKey) = makeAddrAndKey("operator"); @@ -49,7 +47,8 @@ contract BridgeServiceManagerTest is Test, EIP712("BridgeServiceManager", "1") { deal(bob, 1 ether); deal(usdc, bob, 1000 * 10**6); deal(usdc, address(remoteVault), 1000 * 10**6); - deal(operator, 1 ether); } -} \ No newline at end of file + + // Add detailed test functions here +} diff --git a/test/PermissionedBridge.t.sol b/test/PermissionedBridge.t.sol index 7e8f71d..16f1efc 100644 --- a/test/PermissionedBridge.t.sol +++ b/test/PermissionedBridge.t.sol @@ -2,19 +2,18 @@ pragma solidity ^0.8.13; import {Test, console} from "forge-std/Test.sol"; - import "../src/PermissionedBridge.sol"; import "../src/Events.sol"; import {Structs} from "../src/Structs.sol"; - import "openzeppelin/contracts/utils/cryptography/EIP712.sol"; +/// @title PermissionedBridge Test +/// @notice This contract tests the functionalities of the PermissionedBridge contract using Forge test framework contract PermissionedBridgeTest is Test, EIP712("PermissionedBridge", "1") { PermissionedBridge public localVault; PermissionedBridge public remoteVault; address usdc = address(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); - uint256 bridgeFee = 0.005 ether; uint256 crankGasCost = 100_000; @@ -23,6 +22,7 @@ contract PermissionedBridgeTest is Test, EIP712("PermissionedBridge", "1") { address operator; uint256 operatorPrivateKey; + /// @notice Sets up the test environment function setUp() public { (operator, operatorPrivateKey) = makeAddrAndKey("operator"); @@ -34,7 +34,6 @@ contract PermissionedBridgeTest is Test, EIP712("PermissionedBridge", "1") { deal(bob, 1 ether); deal(usdc, bob, 1000 * 10**6); deal(usdc, address(remoteVault), 1000 * 10**6); - deal(operator, 1 ether); localVault.setOperatorWeight(operator, 1000 ether); remoteVault.setOperatorWeight(operator, 1000 ether); @@ -46,14 +45,13 @@ contract PermissionedBridgeTest is Test, EIP712("PermissionedBridge", "1") { ) public returns (bytes memory) { bytes32 digest = forVault.getDigest(data); - (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign( - pkey, - digest - ); + (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(pkey, digest); return abi.encodePacked(r1, s1, v1); } + /// @notice Tests if bridge request emits correct events + /// @return The created bridge request data function testBridgeRequestEmitEvents() public returns (Structs.BridgeRequestData memory) { vm.startPrank(bob); @@ -77,6 +75,7 @@ contract PermissionedBridgeTest is Test, EIP712("PermissionedBridge", "1") { ); } + /// @notice Tests if an operator can submit an attestation function testOperatorCanSubmitAttestation() public { testBridgeRequestEmitEvents(); @@ -100,11 +99,8 @@ contract PermissionedBridgeTest is Test, EIP712("PermissionedBridge", "1") { transferIndex ); - // NOTE: This is signed against the local vault in this example, but for cross-chain swaps this would need to - // be signed against the remoteVault as the chainId is part of the EIP712 signing domain!! bytes memory attestation = signBridgeRequestData(localVault, bridgeRequest, operatorPrivateKey); - vm.prank(operator); vm.expectEmit(true, true, true, true); emit Events.AVSAttestation(attestation, 0, 1000 ether); @@ -112,6 +108,7 @@ contract PermissionedBridgeTest is Test, EIP712("PermissionedBridge", "1") { localVault.publishAttestation(attestation, 0); } + /// @notice Tests if the bridge completes and releases funds function testBridgeCompletesReleaseFunds() public { testBridgeRequestEmitEvents(); @@ -135,8 +132,6 @@ contract PermissionedBridgeTest is Test, EIP712("PermissionedBridge", "1") { transferIndex ); - // NOTE: This is signed against the local vault in this example, but for cross-chain swaps this would need to - // be signed against the remoteVault as the chainId is part of the EIP712 signing domain!! bytes memory attestation = signBridgeRequestData(remoteVault, bridgeRequest, operatorPrivateKey); bytes[] memory bridgeRequestSignatures = new bytes[](1); bridgeRequestSignatures[0] = attestation; diff --git a/test/tests.md b/test/tests.md new file mode 100644 index 0000000..492e533 --- /dev/null +++ b/test/tests.md @@ -0,0 +1,139 @@ +# Testing + +## Introduction +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. + +## Setting Up the Test Environment +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. + +### Example Setup Function +```solidity +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); +} +``` + +Test Cases +Test Case 1: Bridge Request Emission +This test case ensures that a bridge request emits the correct events when created. + +```solidity +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(remoteVault), 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 + ); +} +``` + +Test Case 2: Operator Attestation Submission +This test case verifies that an operator can submit an attestation for a bridge request. + +```solidity +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); +} +``` + +Test Case 3: Fund Release Completion +This test case ensures that the bridge completes and releases funds correctly. + +```solidity +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); +} +``` \ No newline at end of file