From 7e99f8e7c46dd392bc2cf06042d0506889fd37a7 Mon Sep 17 00:00:00 2001 From: Gautham Anant <32277907+gpsanant@users.noreply.github.com> Date: Tue, 20 Jun 2023 09:53:11 -0700 Subject: [PATCH] added contracts (#21) --- .github/workflows/test-contracts.yml | 39 +++ .github/workflows/unit-tests.yml | 12 +- .gitmodules | 24 +- contracts/.gitignore | 14 + contracts/.keep | 0 contracts/compile.sh | 87 ------ contracts/eigenda-contracts/lib/forge-std | 1 - .../lib/openzeppelin-contracts | 1 - .../lib/openzeppelin-contracts-upgradeable | 1 - contracts/eigenlayer-contracts | 1 - contracts/foundry.toml | 25 ++ contracts/lib/eigenlayer-contracts | 1 + contracts/lib/forge-std | 1 + contracts/lib/openzeppelin-contracts | 1 + .../lib/openzeppelin-contracts-upgradeable | 1 + contracts/script/Deployer.s.sol | 194 ++++++++++++++ contracts/src/core/EigenDAServiceManager.sol | 248 ++++++++++++++++++ .../src/core/EigenDAServiceManagerStorage.sol | 49 ++++ .../src/interfaces/IEigenDAServiceManager.sol | 86 ++++++ contracts/src/libraries/DataStoreUtils.sol | 118 +++++++++ 20 files changed, 790 insertions(+), 114 deletions(-) create mode 100644 .github/workflows/test-contracts.yml create mode 100644 contracts/.gitignore delete mode 100644 contracts/.keep delete mode 100755 contracts/compile.sh delete mode 160000 contracts/eigenda-contracts/lib/forge-std delete mode 160000 contracts/eigenda-contracts/lib/openzeppelin-contracts delete mode 160000 contracts/eigenda-contracts/lib/openzeppelin-contracts-upgradeable delete mode 160000 contracts/eigenlayer-contracts create mode 100644 contracts/foundry.toml create mode 160000 contracts/lib/eigenlayer-contracts create mode 160000 contracts/lib/forge-std create mode 160000 contracts/lib/openzeppelin-contracts create mode 160000 contracts/lib/openzeppelin-contracts-upgradeable create mode 100644 contracts/script/Deployer.s.sol create mode 100644 contracts/src/core/EigenDAServiceManager.sol create mode 100644 contracts/src/core/EigenDAServiceManagerStorage.sol create mode 100644 contracts/src/interfaces/IEigenDAServiceManager.sol create mode 100644 contracts/src/libraries/DataStoreUtils.sol diff --git a/.github/workflows/test-contracts.yml b/.github/workflows/test-contracts.yml new file mode 100644 index 000000000..1387324b9 --- /dev/null +++ b/.github/workflows/test-contracts.yml @@ -0,0 +1,39 @@ +name: test-contracts + +on: + push: + pull_request: + types: [opened, reopened] + +env: + FOUNDRY_PROFILE: ci + +concurrency: + group: ${{github.workflow}}-${{github.ref}} + cancel-in-progress: true + +jobs: + check: + name: Foundry Project + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Install forge dependencies + run: forge install + working-directory: ./contracts + + - name: Run tests + run: forge test -vvv + working-directory: ./contracts + + - name: Run snapshot + run: forge snapshot + working-directory: ./contracts diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 6ac7f487d..1a7726b6b 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -20,19 +20,9 @@ jobs: - name: Checkout EigenDA uses: actions/checkout@v3 - - name: Clone EL contracts - uses: actions/checkout@v2 - with: - repository: Layr-Labs/eigenlayer-contracts - branch: master - path: contracts/eigenlayer-contracts - ssh-key: ${{ secrets.EIGENLAYR_DEPLOY_KEY }} - submodules: recursive - persist-credentials: true - - name: Update Submodule Commits run: | - git submodule update --init --recursive contracts/eigenlayer-contracts + git submodule update --init --recursive - name: Install geth run: | diff --git a/.gitmodules b/.gitmodules index c80975a13..953fe4f1c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,12 +1,12 @@ -[submodule "contracts/eigenlayer-contracts"] - path = contracts/eigenlayer-contracts - url = https://github.com/Layr-Labs/eigenlayer-contracts.git -[submodule "contracts/eigenda-contracts/lib/forge-std"] - path = contracts/eigenda-contracts/lib/forge-std - url = https://github.com/foundry-rs/forge-std.git -[submodule "contracts/eigenda-contracts/lib/openzeppelin-contracts"] - path = contracts/eigenda-contracts/lib/openzeppelin-contracts - url = https://github.com/OpenZeppelin/openzeppelin-contracts.git -[submodule "contracts/eigenda-contracts/lib/openzeppelin-contracts-upgradeable"] - path = contracts/eigenda-contracts/lib/openzeppelin-contracts-upgradeable - url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable.git +[submodule "contracts/lib/forge-std"] + path = contracts/lib/forge-std + url = https://github.com/foundry-rs/forge-std +[submodule "contracts/lib/eigenlayer-contracts"] + path = contracts/lib/eigenlayer-contracts + url = https://github.com/Layr-Labs/eigenlayer-contracts +[submodule "contracts/lib/openzeppelin-contracts"] + path = contracts/lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts +[submodule "contracts/lib/openzeppelin-contracts-upgradeable"] + path = contracts/lib/openzeppelin-contracts-upgradeable + url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable diff --git a/contracts/.gitignore b/contracts/.gitignore new file mode 100644 index 000000000..85198aaa5 --- /dev/null +++ b/contracts/.gitignore @@ -0,0 +1,14 @@ +# Compiler files +cache/ +out/ + +# Ignores development broadcast logs +!/broadcast +/broadcast/*/31337/ +/broadcast/**/dry-run/ + +# Docs +docs/ + +# Dotenv file +.env diff --git a/contracts/.keep b/contracts/.keep deleted file mode 100644 index e69de29bb..000000000 diff --git a/contracts/compile.sh b/contracts/compile.sh deleted file mode 100755 index 5e130ce4f..000000000 --- a/contracts/compile.sh +++ /dev/null @@ -1,87 +0,0 @@ -#!/bin/bash - -function compile_el { - mkdir -p data - - pushd eigenlayer-contracts > /dev/null - forge clean - forge build - popd > /dev/null - - contracts="ERC20PresetFixedSupply BLSPublicKeyCompendium BLSRegistry InvestmentManager EigenLayrDelegation InvestmentManager Slasher" - for contract in $contracts; do - create_binding eigenlayer-contracts $contract ../common/contracts/bindings - done -} - -function compile_dl { - mkdir -p data - - cp -r eigenlayer-contracts eigenda-contracts/lib/ - - pushd eigenda-contracts > /dev/null - forge clean - forge build - popd > /dev/null - - rm -rf eigenda-contracts/lib/eigenlayer-contracts - - contracts="DataLayrServiceManager DataLayrChallengeUtils DataLayrChallenge DataLayrPaymentManager" - for contract in $contracts; do - create_binding eigenda-contracts $contract ../common/contracts/bindings - done - - for FILE in eigenda-contracts/out/DataLayr*.sol; do - contract=$(basename -s .sol $FILE) - - if [[ "$contract" != *".t"* ]]; then - create_binding eigenda-contracts $contract ../common/contracts/bindings - fi - done - -} - -function compile_and_test_dl { - compile_dl - - cp -r eigenlayer-contracts eigenda-contracts/lib/ - - pushd eigenda-contracts > /dev/null - forge test --match-test testLoopConfirmDataStore - popd > /dev/null - - rm -rf eigenda-contracts/lib/eigenlayer-contracts -} - -function create_binding { - contract_dir=$1 - contract=$2 - binding_dir=$3 - echo $contract - mkdir -p $binding_dir/${contract} - contract_json="$contract_dir/out/${contract}.sol/${contract}.json" - solc_abi=$(cat ${contract_json} | jq -r '.abi') - solc_bin=$(cat ${contract_json} | jq -r '.bytecode.object') - - echo ${solc_abi} > data/tmp.abi - echo ${solc_bin} > data/tmp.bin - - rm -f $binding_dir/${contract}/binding.go - abigen --bin=data/tmp.bin --abi=data/tmp.abi --pkg=contract${contract} --out=$binding_dir/${contract}/binding.go -} - -case "$1" in - compile-rollup) - compile_rollup ;; - compile-el) - compile_el ;; - compile-dl) - compile_dl ;; - compile-and-test-dl) - compile_and_test_dl ;; - *) - tput setaf 1 - echo "Unknown subcommand" $1 - echo "./compile.sh help" - tput sgr0 ;; -esac diff --git a/contracts/eigenda-contracts/lib/forge-std b/contracts/eigenda-contracts/lib/forge-std deleted file mode 160000 index b00f504da..000000000 --- a/contracts/eigenda-contracts/lib/forge-std +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b00f504daf0bdd8cf2e67973e2c86bd213cda400 diff --git a/contracts/eigenda-contracts/lib/openzeppelin-contracts b/contracts/eigenda-contracts/lib/openzeppelin-contracts deleted file mode 160000 index 253bfa68c..000000000 --- a/contracts/eigenda-contracts/lib/openzeppelin-contracts +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 253bfa68c27785536d58afcc95ebeb4af6f9aa7a diff --git a/contracts/eigenda-contracts/lib/openzeppelin-contracts-upgradeable b/contracts/eigenda-contracts/lib/openzeppelin-contracts-upgradeable deleted file mode 160000 index 2f3e788cd..000000000 --- a/contracts/eigenda-contracts/lib/openzeppelin-contracts-upgradeable +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 2f3e788cd472545adc3ff152ef251e6d10d23927 diff --git a/contracts/eigenlayer-contracts b/contracts/eigenlayer-contracts deleted file mode 160000 index fa80db020..000000000 --- a/contracts/eigenlayer-contracts +++ /dev/null @@ -1 +0,0 @@ -Subproject commit fa80db0202cf74fb2bae3ffc6aa6db988074a698 diff --git a/contracts/foundry.toml b/contracts/foundry.toml new file mode 100644 index 000000000..e420823c8 --- /dev/null +++ b/contracts/foundry.toml @@ -0,0 +1,25 @@ +[profile.default] +src = 'src' +out = 'out' +libs = ['lib'] +fs_permissions = [{ access = "read-write", path = "./"}] + +remappings = [ + "@eigenlayer/=lib/eigenlayer-contracts/src/", + "@openzeppelin/=lib/openzeppelin-contracts/", + "@openzeppelin-upgrades/=lib/openzeppelin-contracts-upgradeable/", + "forge-std/=lib/forge-std/src/" +] + +gas_reports = ["*"] + +# A list of ignored solc error codes + +# Enables or disables the optimizer +optimizer = true +# The number of optimizer runs +optimizer_runs = 200 +# Whether or not to use the Yul intermediate representation compilation pipeline +via_ir = false +# Override the Solidity version (this overrides `auto_detect_solc`) +solc_version = '0.8.12' \ No newline at end of file diff --git a/contracts/lib/eigenlayer-contracts b/contracts/lib/eigenlayer-contracts new file mode 160000 index 000000000..fbbe6ea32 --- /dev/null +++ b/contracts/lib/eigenlayer-contracts @@ -0,0 +1 @@ +Subproject commit fbbe6ea320566ade9177783f8bf3671230d79f53 diff --git a/contracts/lib/forge-std b/contracts/lib/forge-std new file mode 160000 index 000000000..e8a047e3f --- /dev/null +++ b/contracts/lib/forge-std @@ -0,0 +1 @@ +Subproject commit e8a047e3f40f13fa37af6fe14e6e06283d9a060e diff --git a/contracts/lib/openzeppelin-contracts b/contracts/lib/openzeppelin-contracts new file mode 160000 index 000000000..e50c24f58 --- /dev/null +++ b/contracts/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit e50c24f5839db17f46991478384bfda14acfb830 diff --git a/contracts/lib/openzeppelin-contracts-upgradeable b/contracts/lib/openzeppelin-contracts-upgradeable new file mode 160000 index 000000000..bc95521e3 --- /dev/null +++ b/contracts/lib/openzeppelin-contracts-upgradeable @@ -0,0 +1 @@ +Subproject commit bc95521e34dcd49792065e264a7ad2b5a86f0091 diff --git a/contracts/script/Deployer.s.sol b/contracts/script/Deployer.s.sol new file mode 100644 index 000000000..efac8d806 --- /dev/null +++ b/contracts/script/Deployer.s.sol @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.9; + +import "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol"; +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; +import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import "@openzeppelin/contracts/proxy/beacon/IBeacon.sol"; +import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; + +import "@eigenlayer/contracts/interfaces/IDelegationManager.sol"; +import "@eigenlayer/contracts/interfaces/IStrategyManager.sol"; +import "@eigenlayer/contracts/interfaces/IStrategy.sol"; + +import "@eigenlayer/contracts/permissions/PauserRegistry.sol"; + +import "@eigenlayer/contracts/middleware/BLSPublicKeyCompendium.sol"; +import "@eigenlayer/contracts/middleware/BLSRegistry.sol"; +import "@eigenlayer/contracts/middleware/PaymentManager.sol"; + +import "@eigenlayer/test/mocks/EmptyContract.sol"; + +import "../src/core/EigenDAServiceManager.sol"; +import "../src/libraries/DataStoreUtils.sol"; + +import "forge-std/Test.sol"; + +import "forge-std/Script.sol"; +import "forge-std/StdJson.sol"; + +// TODO: REVIEW AND FIX THIS ENTIRE SCRIPT + +// # To load the variables in the .env file +// source .env + +// # To deploy and verify our contract +// forge script script/Deployer.s.sol:EigenDADeployer --rpc-url $RPC_URL --private-key $PRIVATE_KEY --broadcast -vvvv + +//TODO: encode data properly so that we initialize ITransparentUpgradeableProxy contracts in their constructor rather than a separate call (if possible) +contract EigenDADeployer is Script, Test { + uint256 public constant DURATION_SCALE = 1 hours; + + // EigenLayer contracts + IDelegationManager public delegationManager; + IStrategyManager public strategyManager; + ISlasher public slasher; + + // EigenDA contracts + ProxyAdmin public eigenDAProxyAdmin; + PauserRegistry public eigenDAPauserReg; + + BLSPublicKeyCompendium public pubkeyCompendium; + BLSRegistry public dlReg; + EigenDAServiceManager public dlsm; + PaymentManager public eigenDAPaymentManager; + + BLSPublicKeyCompendium public pubkeyCompendiumImplementation; + BLSRegistry public dlRegImplementation; + EigenDAServiceManager public dlsmImplementation; + PaymentManager public eigenDAPaymentManagerImplementation; + + // testing/mock contracts + IERC20 public weth; + IStrategy public wethStrat; + IStrategy public eigenStrat; + EmptyContract public emptyContract; + + uint256 public gasLimit = 750000; + + // deploy all the EigenDA contracts. Relies on many EL contracts having already been deployed. + function run() external { + address pauser = msg.sender; + address unpauser = msg.sender; + address eigenDAReputedMultisig = msg.sender; + address eigenDATeamMultisig = msg.sender; + + string memory deployConfigJson = vm.readFile("data/eigenDA_deploy_config.json"); + weth = IERC20(stdJson.readAddress(deployConfigJson, ".weth")); + strategyManager = IStrategyManager(stdJson.readAddress(deployConfigJson, ".strategyManager")); + delegationManager = IDelegationManager(stdJson.readAddress(deployConfigJson, ".delegationManager")); + slasher = strategyManager.slasher(); + // slasher = ISlasher(stdJson.readAddress(deployConfigJson, ".slasher")); + wethStrat = IStrategy(stdJson.readAddress(deployConfigJson, ".wethStrat")); + eigenStrat = IStrategy(stdJson.readAddress(deployConfigJson, ".eigenStrat")); + + emit log_address(address(weth)); + + vm.startBroadcast(); + + // deploy proxy admin for ability to upgrade proxy contracts + eigenDAProxyAdmin = new ProxyAdmin(); + + // deploy pauser registry + eigenDAPauserReg = new PauserRegistry(pauser, unpauser); + + emptyContract = new EmptyContract(); + + // hard-coded inputs + uint256 feePerBytePerTime = 1; + uint256 _paymentFraudproofCollateral = 1e16; + + /** + * First, deploy upgradeable proxy contracts that **will point** to the implementations. Since the implementation contracts are + * not yet deployed, we give these proxies an empty contract as the initial implementation, to act as if they have no code. + */ + dlsm = EigenDAServiceManager( + address(new TransparentUpgradeableProxy(address(emptyContract), address(eigenDAProxyAdmin), "")) + ); + pubkeyCompendium = BLSPublicKeyCompendium( + address(new TransparentUpgradeableProxy(address(emptyContract), address(eigenDAProxyAdmin), "")) + ); + dlReg = BLSRegistry( + address(new TransparentUpgradeableProxy(address(emptyContract), address(eigenDAProxyAdmin), "")) + ); + + // Second, deploy the *implementation* contracts, using the *proxy contracts* as inputs + dlsmImplementation = new EigenDAServiceManager( + IBLSStakeRegistryCoordinator(address(dlReg)), + strategyManager, + delegationManager, + slasher, + eigenDAPaymentManager + ); + pubkeyCompendiumImplementation = new BLSPublicKeyCompendium(); + eigenDAPaymentManagerImplementation = new PaymentManager( + dlsm, + weth + ); + { + dlRegImplementation = new BLSRegistry( + strategyManager, + dlsm, + pubkeyCompendium + ); + } + + // Third, upgrade the proxy contracts to use the correct implementation contracts and initialize them. + { + uint16 quorumThresholdBasisPoints = 9000; + uint16 adversaryThresholdBasisPoints = 4000; + eigenDAProxyAdmin.upgradeAndCall( + ITransparentUpgradeableProxy(payable(address(dlsm))), + address(dlsmImplementation), + abi.encodeWithSelector( + EigenDAServiceManager.initialize.selector, + eigenDAPauserReg, + eigenDAReputedMultisig, + quorumThresholdBasisPoints, + adversaryThresholdBasisPoints, + feePerBytePerTime, + eigenDATeamMultisig + ) + ); + } + eigenDAProxyAdmin.upgrade( + ITransparentUpgradeableProxy(payable(address(pubkeyCompendium))), + address(pubkeyCompendiumImplementation) + ); + eigenDAProxyAdmin.upgradeAndCall( + ITransparentUpgradeableProxy(payable(address(eigenDAPaymentManager))), + address(eigenDAPaymentManagerImplementation), + abi.encodeWithSelector(PaymentManager.initialize.selector, eigenDAPauserReg, _paymentFraudproofCollateral) + ); + + { + uint96 multiplier = 1e18; + uint8 _NUMBER_OF_QUORUMS = 2; + uint256[] memory _quorumBips = new uint256[](_NUMBER_OF_QUORUMS); + // split 60% ETH quorum, 40% EIGEN quorum + _quorumBips[0] = 6000; + _quorumBips[1] = 4000; + VoteWeigherBaseStorage.StrategyAndWeightingMultiplier[] memory ethStratsAndMultipliers = + new VoteWeigherBaseStorage.StrategyAndWeightingMultiplier[](1); + ethStratsAndMultipliers[0].strategy = wethStrat; + ethStratsAndMultipliers[0].multiplier = multiplier; + VoteWeigherBaseStorage.StrategyAndWeightingMultiplier[] memory eigenStratsAndMultipliers = + new VoteWeigherBaseStorage.StrategyAndWeightingMultiplier[](1); + eigenStratsAndMultipliers[0].strategy = eigenStrat; + eigenStratsAndMultipliers[0].multiplier = multiplier; + + eigenDAProxyAdmin.upgradeAndCall( + ITransparentUpgradeableProxy(payable(address(dlReg))), + address(dlRegImplementation), + abi.encodeWithSelector(BLSRegistry.initialize.selector, _quorumBips, ethStratsAndMultipliers, eigenStratsAndMultipliers) + ); + } + + vm.writeFile("data/dlsm.addr", vm.toString(address(dlsm))); + vm.writeFile("data/dlReg.addr", vm.toString(address(dlReg))); + vm.writeFile("data/pubkeyCompendium.addr", vm.toString(address(pubkeyCompendium))); + + vm.stopBroadcast(); + } +} diff --git a/contracts/src/core/EigenDAServiceManager.sol b/contracts/src/core/EigenDAServiceManager.sol new file mode 100644 index 000000000..9cad81a6b --- /dev/null +++ b/contracts/src/core/EigenDAServiceManager.sol @@ -0,0 +1,248 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.9; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol"; +import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol"; + +import "@eigenlayer/contracts/middleware/BLSSignatureChecker.sol"; + +import "@eigenlayer/contracts/interfaces/IDelegationManager.sol"; +import "@eigenlayer/contracts/interfaces/IDelegationTerms.sol"; +import "@eigenlayer/contracts/interfaces/IPaymentManager.sol"; + +import "@eigenlayer/contracts/libraries/BytesLib.sol"; +import "@eigenlayer/contracts/libraries/Merkle.sol"; +import "@eigenlayer/contracts/permissions/Pausable.sol"; + +import "../libraries/DataStoreUtils.sol"; + +import "./EigenDAServiceManagerStorage.sol"; + +/** + * @title Primary entrypoint for procuring services from EigenDA. + * @author Layr Labs, Inc. + * @notice This contract is used for: + * - initializing the data store by the disperser + * - confirming the data store by the disperser with inferred aggregated signatures of the quorum + * - freezing operators as the result of various "challenges" + */ +contract EigenDAServiceManager is Initializable, OwnableUpgradeable, EigenDAServiceManagerStorage, BLSSignatureChecker, Pausable { + using BytesLib for bytes; + + uint8 internal constant PAUSED_CONFIRM_DATASTORE = 0; + + /** + * @notice The EigenLayer delegation contract for this EigenDA which is primarily used by + * delegators to delegate their stake to operators who would serve as EigenDA + * nodes and so on. + * @dev For more details, see DelegationManager.sol. + */ + IDelegationManager public immutable delegationManager; + + IStrategyManager public immutable strategyManager; + + ISlasher public immutable slasher; + + /** + * @notice contract used for handling payment challenges + */ + IPaymentManager public immutable eigenDAPaymentManager; + + /// @notice when applied to a function, ensures that the function is only callable by the `registryCoordinator`. + modifier onlyRegistryCoordinator() { + require(msg.sender == address(registryCoordinator), "onlyRegistryCoordinator: not from registry coordinator"); + _; + } + + /// @notice when applied to a function, ensures that the function is only callable by the `feeSetter`. + modifier onlyFeeSetter() { + require(msg.sender == feeSetter, "onlyFeeSetter: not from fee setter"); + _; + } + + constructor( + IBLSStakeRegistryCoordinator _registryCoordinator, + IStrategyManager _strategyManager, + IDelegationManager _delegationMananger, + ISlasher _slasher, + IPaymentManager _eigenDAPaymentManager + ) + BLSSignatureChecker(_registryCoordinator) + { + strategyManager = _strategyManager; + delegationManager = _delegationMananger; + slasher = _slasher; + eigenDAPaymentManager = _eigenDAPaymentManager; + _disableInitializers(); + } + + function initialize( + IPauserRegistry _pauserRegistry, + address initialOwner, + uint256 _feePerBytePerTime, + address _feeSetter + ) + public + initializer + { + _initializePauser(_pauserRegistry, UNPAUSE_ALL); + _transferOwnership(initialOwner); + _setFeePerBytePerTime(_feePerBytePerTime); + _setFeeSetter(_feeSetter); + } + + /// @notice Used by EigenDA governance to adjust the address of the `feeSetter`, which can adjust the value of `feePerBytePerTime`. + function setFeeSetter(address _feeSetter) + external + onlyOwner + { + _setFeeSetter(_feeSetter); + } + + /** + * @notice This function is used for + * - submitting data availabilty certificates, + * - check that the aggregate signature is valid, + * - and check whether quorum has been achieved or not. + */ + function confirmDataStore( + DataStoreHeader calldata dataStoreHeader, + NonSignerStakesAndSignature memory nonSignerStakesAndSignature + ) external onlyWhenNotPaused(PAUSED_CONFIRM_DATASTORE) { + // make sure the information needed to derive the non-signers and dataStore is in calldata to avoid emitting events + require(tx.origin == msg.sender, "EigenDAServiceManager.confirmDataStore: header and nonsigner data must be in calldata"); + // make sure the stakes against which the DataStore is being confirmed are not stale + require( + dataStoreHeader.referenceBlockNumber <= block.number, "EigenDAServiceManager.initDataStore: specified referenceBlockNumber is in future" + ); + + require( + (dataStoreHeader.referenceBlockNumber + BLOCK_STALE_MEASURE) >= uint32(block.number), + "EigenDAServiceManager.initDataStore: specified referenceBlockNumber is too far in past" + ); + + // TODO: make sure the size of the DataStore is not too large + + // calculate fee and charge confirmer + uint96 fee = 0; + // TODO: Deduct fee + + // calculate dataStoreHeaderHash which nodes signed + bytes32 dataStoreHeaderHash = keccak256(abi.encode(dataStoreHeader)); + + // check the signature + ( + QuorumStakeTotals memory quorumStakeTotals, + bytes32 signatoryRecordHash + ) = checkSignatures( + dataStoreHeaderHash, + dataStoreHeader.quorumNumbers, // use list of uint8s instead of uint256 bitmap to not iterate 256 times + dataStoreHeader.referenceBlockNumber, + nonSignerStakesAndSignature + ); + + // check that signatories own at least a threshold percentage of each quourm + for (uint i = 0; i < dataStoreHeader.quorumThresholdPercentages.length; i++) { + // we don't check that the quorumThresholdPercentages are not >100 because a greater value would trivially fail the check, implying + // signed stake > total stake + require( + quorumStakeTotals.signedStakeForQuorum[i] * THRESHOLD_DENOMINATOR >= + quorumStakeTotals.totalStakeForQuorum[i] * uint8(dataStoreHeader.quorumThresholdPercentages[i]), + "EigenDAServiceManager.confirmDataStore: signatories do not own at least threshold percentage of a quorum" + ); + } + + // store the metadata hash + uint32 dataStoreIdMemory = dataStoreId; + dataStoreIdToDataStoreMetadataHash[dataStoreIdMemory] = DataStoreUtils.hashDataStoreHashedMetadata(dataStoreHeaderHash, signatoryRecordHash, fee, uint32(block.number)); + + emit DataStoreConfirmed(dataStoreIdMemory, fee); + + // increment the dataStoreId + dataStoreId = dataStoreIdMemory + 1; + } + + /// @notice Called in the event of challenge resolution, in order to forward a call to the Slasher, which 'freezes' the `operator`. + function freezeOperator(address /*operator*/) external { + revert("EigenDAServiceManager.freezeOperator: not implemented"); + // require( + // msg.sender == address(eigenDAChallenge) + // || msg.sender == address(eigenDABombVerifier), + // "EigenDAServiceManager.freezeOperator: Only challenge resolvers can slash operators" + // ); + // slasher.freezeOperator(operator); + } + + /** + * @notice Called by the Registry in the event of a new registration, to forward a call to the Slasher + * @param operator The operator whose stake is being updated + * @param serveUntilBlock The block until which the stake accounted for in the first update is slashable by this middleware + */ + function recordFirstStakeUpdate(address operator, uint32 serveUntilBlock) external onlyRegistryCoordinator { + slasher.recordFirstStakeUpdate(operator, serveUntilBlock); + } + + /** + * @notice Called by the registryCoordinator, in order to forward a call to the Slasher, informing it of a stake update + * @param operator The operator whose stake is being updated + * @param updateBlock The block at which the update is being made + * @param serveUntilBlock The block until which the stake withdrawn from the operator in this update is slashable by this middleware + * @param prevElement The value of the previous element in the linked list of stake updates (generated offchain) + */ + function recordStakeUpdate(address operator, uint32 updateBlock, uint32 serveUntilBlock, uint256 prevElement) external onlyRegistryCoordinator { + slasher.recordStakeUpdate(operator, updateBlock, serveUntilBlock, prevElement); + } + + /** + * @notice Called by the registryCoordinator in the event of deregistration, to forward a call to the Slasher + * @param operator The operator being deregistered + * @param serveUntilBlock The block until which the stake delegated to the operator is slashable by this middleware + */ + function recordLastStakeUpdateAndRevokeSlashingAbility(address operator, uint32 serveUntilBlock) external onlyRegistryCoordinator { + slasher.recordLastStakeUpdateAndRevokeSlashingAbility(operator, serveUntilBlock); + } + + /** + * @notice Used by the feeSetter to adjust the value of the `feePerBytePerTime` variable. + * @param _feePerBytePerTime The new value of the `feePerBytePerTime` variable. + */ + function setFeePerBytePerTime(uint256 _feePerBytePerTime) external onlyFeeSetter { + _setFeePerBytePerTime(_feePerBytePerTime); + } + + // VIEW FUNCTIONS + function taskNumber() external view returns (uint32) { + return dataStoreId; + } + + /// @notice Returns the block until which operators must serve. + function latestServeUntilBlock() external view returns (uint32) { + return uint32(block.number) + STORE_DURATION_BLOCKS + BLOCK_STALE_MEASURE; + } + + /// @dev need to override function here since its defined in both these contracts + function owner() public view override(OwnableUpgradeable, IServiceManager) returns (address) { + return OwnableUpgradeable.owner(); + } + + // INTERNAL FUNTIONS + + function calculateFee(uint256 totalBytes, uint256 _feePerBytePerTime, uint32 storePeriodLength) + public + pure + returns (uint256) + { + return uint256(totalBytes * _feePerBytePerTime * storePeriodLength); + } + + function _setFeePerBytePerTime(uint256 _feePerBytePerTime) internal { + emit FeePerBytePerTimeSet(feePerBytePerTime, _feePerBytePerTime); + feePerBytePerTime = _feePerBytePerTime; + } + + function _setFeeSetter(address _feeSetter) internal { + emit FeeSetterChanged(feeSetter, _feeSetter); + feeSetter = _feeSetter; + } +} \ No newline at end of file diff --git a/contracts/src/core/EigenDAServiceManagerStorage.sol b/contracts/src/core/EigenDAServiceManagerStorage.sol new file mode 100644 index 000000000..ec7de495c --- /dev/null +++ b/contracts/src/core/EigenDAServiceManagerStorage.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.9; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@eigenlayer/contracts/interfaces/IDelegationManager.sol"; +import "@eigenlayer/contracts/interfaces/IServiceManager.sol"; +import "@eigenlayer/contracts/interfaces/IStrategyManager.sol"; + +import "../interfaces/IEigenDAServiceManager.sol"; + +/** + * @title Storage variables for the `EigenDAServiceManager` contract. + * @author Layr Labs, Inc. + * @notice This storage contract is separate from the logic to simplify the upgrade process. + */ +abstract contract EigenDAServiceManagerStorage is IEigenDAServiceManager { + // CONSTANTS + uint256 public constant THRESHOLD_DENOMINATOR = 100; + + //TODO: mechanism to change any of these values? + /// @notice Unit of measure (in blocks) for which data will be stored for after confirmation. + uint32 public constant STORE_DURATION_BLOCKS = 2 weeks / 12 seconds; + + /// @notice Minimum DataStore size, in bytes. + uint32 internal constant MIN_STORE_SIZE = 32; + /// @notice Maximum DataStore size, in bytes. + uint32 internal constant MAX_STORE_SIZE = 4e9; + /** + * @notice The maximum amount of blocks in the past that the service will consider stake amounts to still be 'valid'. + * @dev To clarify edge cases, the middleware can look `BLOCK_STALE_MEASURE` blocks into the past, i.e. it may trust stakes from the interval + * [block.number - BLOCK_STALE_MEASURE, block.number] (specifically, *inclusive* of the block that is `BLOCK_STALE_MEASURE` before the current one) + * @dev BLOCK_STALE_MEASURE should be greater than the number of blocks till finalization, but not too much greater, as it is the amount of + * time that nodes can be active after they have deregistered. The larger it is, the farther back stakes can be used, but the longer operators + * have to serve after they've deregistered. + */ + uint32 public constant BLOCK_STALE_MEASURE = 150; + + /// @notice service fee that will be paid out by the disperser to the EigenDA nodes for storing data, per byte stored per unit time (second). + uint256 public feePerBytePerTime; + + /// @notice The current dataStoreId + uint32 public dataStoreId; + + /// @notice Address that has exclusive power to adjust the `feePerBytePerTime` parameter. This address can be set by the contract owner. + address public feeSetter; + + /// @notice mapping between the dataStoreId to the hash of the metadata of the corresponding DataStore + mapping(uint32 => bytes32) public dataStoreIdToDataStoreMetadataHash; +} \ No newline at end of file diff --git a/contracts/src/interfaces/IEigenDAServiceManager.sol b/contracts/src/interfaces/IEigenDAServiceManager.sol new file mode 100644 index 000000000..7da3e4cf1 --- /dev/null +++ b/contracts/src/interfaces/IEigenDAServiceManager.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.9; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@eigenlayer/contracts/interfaces/IServiceManager.sol"; +import "@eigenlayer/contracts/interfaces/IDelayedService.sol"; +import "@eigenlayer/contracts/libraries/BN254.sol"; +import "@eigenlayer/contracts/middleware/BLSSignatureChecker.sol"; +import "@eigenlayer/contracts/interfaces/IDelegationManager.sol"; +import "@eigenlayer/contracts/interfaces/IPaymentManager.sol"; + +interface IEigenDAServiceManager is IServiceManager, IDelayedService { + // EVENTS + + /** + * @notice Emitted when a DataStore is confirmed. + * @param dataStoreId The ID for the DataStore inside of the specified duration (i.e. *not* the globalDataStoreId) + */ + event DataStoreConfirmed(uint32 dataStoreId, uint96 fee); + + event FeePerBytePerTimeSet(uint256 previousValue, uint256 newValue); + + event PaymentManagerSet(address previousAddress, address newAddress); + + event FeeSetterChanged(address previousAddress, address newAddress); + + // STRUCTS + + struct SecurityParam { + uint8 quorumNumber; + uint8 adversaryThresholdPercentages; + } + + struct BlobHeader { + BN254.G1Point commitment; + BN254.G1Point lengthProof; // the proof that the length of the claimed length of the blob + uint32 length; // the length of the blob in coefficients of the polynomial + SecurityParam[] securityParams; + } + + struct DataStoreHeader { + bytes32 blobHeadersRoot; + bytes quorumNumbers; // each byte is a different quorum number + bytes quorumThresholdPercentages; // every bytes is an amount less than 100 specifying the percentage of stake + // the must have signed in the corresponding quorum in `quorumNumbers` + uint32 referenceBlockNumber; + } + + // Relevant metadata for a given datastore + struct DataStoreMetadata { + DataStoreHeader dataStoreHeader; // the header of the data store + bytes32 signatoryRecordHash; // the hash of the signatory record + uint96 fee; // the amount of paymentToken paid for the datastore + uint32 blockNumber; // the block number at which the datastore was confirmed + } + + // Relevant metadata for a given datastore + struct DataStoreMetadataWithSignatoryRecord { + bytes32 dataStoreHeaderHash; // the header hash of the data store + uint32 referenceBlockNumber; // the block number at which stakes + bytes32[] nonSignerPubkeyHashes; // the pubkeyHashes of all of the nonSigners + uint96 fee; // the amount of paymentToken paid for the datastore + uint32 blockNumber; // the block number at which the datastore was confirmed + } + + // FUNCTIONS + + /// @notice mapping between the dataStoreId to the hash of the metadata of the corresponding DataStore + function dataStoreIdToDataStoreMetadataHash(uint32 dataStoreId) external view returns(bytes32); + + /** + * @notice This function is used for + * - submitting data availabilty certificates, + * - check that the aggregate signature is valid, + * - and check whether quorum has been achieved or not. + */ + function confirmDataStore( + DataStoreHeader calldata dataStoreHeader, + BLSSignatureChecker.NonSignerStakesAndSignature memory nonSignerStakesAndSignature + ) external; + + /** + * @notice contract used for handling payments from confirmers to operators + */ + function eigenDAPaymentManager() external view returns (IPaymentManager); +} diff --git a/contracts/src/libraries/DataStoreUtils.sol b/contracts/src/libraries/DataStoreUtils.sol new file mode 100644 index 000000000..1c05aa6e6 --- /dev/null +++ b/contracts/src/libraries/DataStoreUtils.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.9; + +import "@eigenlayer/contracts/libraries/Merkle.sol"; +import "../interfaces/IEigenDAServiceManager.sol"; + +/** + * @title Library of functions shared across EigenDA. + * @author Layr Labs, Inc. + */ +library DataStoreUtils { + // STRUCTS + struct BlobVerificationProof { + uint32 dataStoreId; + IEigenDAServiceManager.DataStoreMetadata dataStoreMetadata; + bytes inclusionProof; + uint8 blobIndex; + bytes quorumThresholdIndexes; + } + + /** + * @notice hashes the given metdata into the commitment that will be stored in the contract + * @param dataStoreHeaderHash the hash of the dataStoreHeader + * @param signatoryRecordHash the hash of the signatory record + * @param fee the fee paid in paymentToken for the dataStore + * @param blockNumber the block number at which the dataStore was confirmed + */ + function hashDataStoreHashedMetadata( + bytes32 dataStoreHeaderHash, + bytes32 signatoryRecordHash, + uint96 fee, + uint32 blockNumber + ) internal pure returns(bytes32) { + return keccak256(abi.encodePacked(dataStoreHeaderHash, signatoryRecordHash, fee, blockNumber)); + } + + /** + * @notice given the a dataStoreHeader in the provided metdata, calculates the hash of the dataStoreMetadata + * @param dataStoreMetadata the metadata of the dataStore + * @return the hash of the dataStoreMetadata + */ + function hashDataStoreMetadata( + IEigenDAServiceManager.DataStoreMetadata calldata dataStoreMetadata + ) internal pure returns(bytes32) { + return hashDataStoreHashedMetadata( + keccak256(abi.encode(dataStoreMetadata.dataStoreHeader)), + dataStoreMetadata.signatoryRecordHash, + dataStoreMetadata.fee, + dataStoreMetadata.blockNumber + ); + } + + /** + * @param blobHeader the blob header to hash + */ + function hashBlobHeader(IEigenDAServiceManager.BlobHeader calldata blobHeader) internal pure returns(bytes32) { + return keccak256(abi.encode(blobHeader)); + } + + /** + * @notice Verifies the inclusion of a blob within a dataStore confirmed in `eigenDAServiceManager` and its trust assumptions + * @param eigenDAServiceManager the contract in which the dataStore was confirmed + * @param blobVerificationProof the relevant data needed to prove inclusion of the blob and that the trust assumptions were as expected + * @param expectedSecurityParamsHash the hash of the expected securityParams for the blob (i.e. its security assumptions on DA) + * @param blobHeader the header of the blob containing relevant attributes of the blob + */ + function verifyBlob( + IEigenDAServiceManager eigenDAServiceManager, + BlobVerificationProof calldata blobVerificationProof, + bytes32 expectedSecurityParamsHash, + IEigenDAServiceManager.BlobHeader calldata blobHeader + ) internal view { + require( + hashDataStoreMetadata(blobVerificationProof.dataStoreMetadata) + == eigenDAServiceManager.dataStoreIdToDataStoreMetadataHash(blobVerificationProof.dataStoreId), + "DataStoreUtils.verifyBlob: dataStoreMetadata does not match stored metadata" + ); + + require( + Merkle.verifyInclusionKeccak( + blobVerificationProof.inclusionProof, + blobVerificationProof.dataStoreMetadata.dataStoreHeader.blobHeadersRoot, + hashBlobHeader(blobHeader), + blobVerificationProof.blobIndex + ), + "DataStoreUtils.verifyBlob: inclusion proof is invalid" + ); + + require(keccak256(abi.encode(blobHeader.securityParams)) == expectedSecurityParamsHash, + "DataStoreUtils.verifyBlob: blob headerQuorumParams were not eppected" + ); + + // make sure that the number of quorumIndexes matches the number of quorumThresholds + uint256 quorumThresholdIndexesLength = blobVerificationProof.quorumThresholdIndexes.length; + require(blobHeader.securityParams.length == quorumThresholdIndexesLength, + "DataStoreUtils.verifyBlob: blob securityParams length does not match quorumThresholdIndexes length" + ); + + // for each quorumThresholdIndex, make sure that the quorumNumber in the DataStoreHeader matches, + // that the quorumThreshold was greater than the adversary threshold + for (uint i = 0; i < quorumThresholdIndexesLength;) { + require( + blobHeader.securityParams[i].quorumNumber == + uint8(blobVerificationProof.dataStoreMetadata.dataStoreHeader.quorumNumbers[uint8(blobVerificationProof.quorumThresholdIndexes[i])]), + "DataStoreUtils.verifyBlob: quorumNumber does not match" + ); + require( + blobHeader.securityParams[i].adversaryThresholdPercentages < + uint8(blobVerificationProof.dataStoreMetadata.dataStoreHeader.quorumThresholdPercentages[uint8(blobVerificationProof.quorumThresholdIndexes[i])]), + "DataStoreUtils.verifyBlob: threshold does not match" + ); + unchecked { + ++i; + } + } + } +}