-
Notifications
You must be signed in to change notification settings - Fork 141
Beefy client wrapper contract #1668
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
6657bb2
6bd9ca8
919f503
5828412
e43ab5f
39bf3fc
c7da3c1
70b59ff
1d3e81d
7c94d09
9b8fd88
d1f0576
6228a56
f9b4923
e919c32
70ac4b0
e3112a2
f086bcf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| // SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com> | ||
| pragma solidity 0.8.33; | ||
|
|
||
| import {Script} from "forge-std/Script.sol"; | ||
| import {console} from "forge-std/console.sol"; | ||
| import {BeefyClientWrapper} from "../src/BeefyClientWrapper.sol"; | ||
|
|
||
| contract DeployBeefyClientWrapper is Script { | ||
| struct Config { | ||
| address beefyClient; | ||
| address owner; | ||
| uint256 maxGasPrice; | ||
| uint256 maxRefundAmount; | ||
| uint256 refundTarget; | ||
| } | ||
|
|
||
| function readConfig() internal returns (Config memory config) { | ||
| config = Config({ | ||
| beefyClient: vm.envAddress("BEEFY_CLIENT_ADDRESS"), | ||
| owner: vm.envAddress("WRAPPER_OWNER"), | ||
| maxGasPrice: vm.envOr("MAX_GAS_PRICE", uint256(100 gwei)), | ||
| maxRefundAmount: vm.envOr("MAX_REFUND_AMOUNT", uint256(0.05 ether)), | ||
| refundTarget: vm.envOr("REFUND_TARGET", uint256(350)) // ~35 min for 100% refund | ||
| }); | ||
| } | ||
|
|
||
| function run() public { | ||
| vm.startBroadcast(); | ||
|
|
||
| Config memory config = readConfig(); | ||
|
|
||
| BeefyClientWrapper wrapper = new BeefyClientWrapper( | ||
| config.beefyClient, | ||
| config.owner, | ||
| config.maxGasPrice, | ||
| config.maxRefundAmount, | ||
| config.refundTarget | ||
| ); | ||
|
|
||
| console.log("BeefyClientWrapper:", address(wrapper)); | ||
|
|
||
| vm.stopBroadcast(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,267 @@ | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| // SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com> | ||
| pragma solidity 0.8.33; | ||
|
|
||
| import {IBeefyClient} from "./interfaces/IBeefyClient.sol"; | ||
|
|
||
| /** | ||
| * @title BeefyClientWrapper | ||
| * @dev Forwards BeefyClient submissions and refunds gas costs to relayers. | ||
| * Anyone can relay. Refunds are only paid when the relayer advances the light | ||
| * client by at least `refundTarget` blocks, ensuring meaningful progress. | ||
| */ | ||
| contract BeefyClientWrapper { | ||
| event GasCredited(address indexed relayer, bytes32 indexed commitmentHash, uint256 gasUsed); | ||
| event SubmissionRefunded(address indexed relayer, uint256 progress, uint256 refundAmount, uint256 totalGasUsed); | ||
| event FundsDeposited(address indexed depositor, uint256 amount); | ||
| event FundsWithdrawn(address indexed recipient, uint256 amount); | ||
|
|
||
| error Unauthorized(); | ||
| error InvalidAddress(); | ||
| error NotTicketOwner(); | ||
| error TransferFailed(); | ||
|
|
||
| address public owner; | ||
| IBeefyClient public beefyClient; | ||
|
|
||
| // Ticket tracking (for multi-step submission) | ||
| mapping(bytes32 => address) public ticketOwner; | ||
| mapping(bytes32 => uint256) public creditedGas; | ||
|
|
||
| // Refund configuration | ||
| uint256 public maxGasPrice; | ||
| uint256 public maxRefundAmount; | ||
|
|
||
| // Progress-based refund target | ||
| uint256 public refundTarget; // Blocks of progress for 100% gas refund (e.g., 350 = ~35 min) | ||
|
|
||
| // Highest commitment block number currently in progress (helps relayers avoid duplicate work) | ||
| uint256 public highestPendingBlock; | ||
| uint256 public highestPendingBlockTimestamp; | ||
|
|
||
| constructor( | ||
| address _beefyClient, | ||
| address _owner, | ||
| uint256 _maxGasPrice, | ||
| uint256 _maxRefundAmount, | ||
| uint256 _refundTarget | ||
| ) { | ||
| if (_beefyClient == address(0) || _owner == address(0)) { | ||
| revert InvalidAddress(); | ||
| } | ||
|
|
||
| beefyClient = IBeefyClient(_beefyClient); | ||
| owner = _owner; | ||
| maxGasPrice = _maxGasPrice; | ||
| maxRefundAmount = _maxRefundAmount; | ||
| refundTarget = _refundTarget; | ||
| } | ||
|
|
||
| /* Beefy Client Proxy Functions */ | ||
|
|
||
| function submitInitial( | ||
| IBeefyClient.Commitment calldata commitment, | ||
| uint256[] calldata bitfield, | ||
| IBeefyClient.ValidatorProof calldata proof | ||
| ) external { | ||
| uint256 startGas = gasleft(); | ||
|
|
||
| beefyClient.submitInitial(commitment, bitfield, proof); | ||
|
|
||
| bytes32 commitmentHash = beefyClient.computeCommitmentHash(commitment); | ||
| ticketOwner[commitmentHash] = msg.sender; | ||
|
|
||
| // Track highest pending block so other relayers can check before starting | ||
| if (commitment.blockNumber > highestPendingBlock) { | ||
| highestPendingBlock = commitment.blockNumber; | ||
| highestPendingBlockTimestamp = block.timestamp; | ||
| } | ||
|
|
||
| _creditGas(startGas, commitmentHash); | ||
| } | ||
|
|
||
| function commitPrevRandao(bytes32 commitmentHash) external { | ||
| uint256 startGas = gasleft(); | ||
|
|
||
| if (ticketOwner[commitmentHash] != msg.sender) { | ||
| revert NotTicketOwner(); | ||
| } | ||
|
|
||
| beefyClient.commitPrevRandao(commitmentHash); | ||
|
|
||
| _creditGas(startGas, commitmentHash); | ||
| } | ||
|
|
||
| function submitFinal( | ||
| IBeefyClient.Commitment calldata commitment, | ||
| uint256[] calldata bitfield, | ||
| IBeefyClient.ValidatorProof[] calldata proofs, | ||
| IBeefyClient.MMRLeaf calldata leaf, | ||
| bytes32[] calldata leafProof, | ||
| uint256 leafProofOrder | ||
| ) external { | ||
| uint256 startGas = gasleft(); | ||
|
|
||
| // Capture previous state for progress calculation | ||
| uint64 previousBeefyBlock = beefyClient.latestBeefyBlock(); | ||
|
|
||
| bytes32 commitmentHash = beefyClient.computeCommitmentHash(commitment); | ||
| if (ticketOwner[commitmentHash] != msg.sender) { | ||
| revert NotTicketOwner(); | ||
| } | ||
|
|
||
| beefyClient.submitFinal(commitment, bitfield, proofs, leaf, leafProof, leafProofOrder); | ||
|
|
||
| // Calculate progress | ||
| uint256 progress = commitment.blockNumber - previousBeefyBlock; | ||
|
|
||
| // Clear highest pending block if light client has caught up | ||
| if (beefyClient.latestBeefyBlock() >= highestPendingBlock) { | ||
| highestPendingBlock = 0; | ||
| highestPendingBlockTimestamp = 0; | ||
| } | ||
|
|
||
| uint256 previousGas = creditedGas[commitmentHash]; | ||
| delete creditedGas[commitmentHash]; | ||
| delete ticketOwner[commitmentHash]; | ||
|
|
||
| _refundWithProgress(startGas, previousGas, progress); | ||
| } | ||
|
|
||
| function submitFiatShamir( | ||
| IBeefyClient.Commitment calldata commitment, | ||
| uint256[] calldata bitfield, | ||
| IBeefyClient.ValidatorProof[] calldata proofs, | ||
| IBeefyClient.MMRLeaf calldata leaf, | ||
| bytes32[] calldata leafProof, | ||
| uint256 leafProofOrder | ||
| ) external { | ||
| beefyClient.submitFiatShamir(commitment, bitfield, proofs, leaf, leafProof, leafProofOrder); | ||
|
|
||
| // Clear highest pending block if light client has caught up | ||
| if (beefyClient.latestBeefyBlock() >= highestPendingBlock) { | ||
| highestPendingBlock = 0; | ||
| highestPendingBlockTimestamp = 0; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * @dev Abandon a ticket. Useful if another relayer is competing for the same commitment. | ||
| * Credited gas is forfeited when clearing a ticket. | ||
| */ | ||
| function clearTicket(bytes32 commitmentHash) external { | ||
| if (ticketOwner[commitmentHash] != msg.sender) { | ||
| revert NotTicketOwner(); | ||
| } | ||
|
|
||
| delete creditedGas[commitmentHash]; | ||
| delete ticketOwner[commitmentHash]; | ||
| } | ||
|
|
||
| /* Internal Functions */ | ||
|
|
||
| function _checkOwner() internal view { | ||
| if (msg.sender != owner) { | ||
| revert Unauthorized(); | ||
| } | ||
| } | ||
|
|
||
| function _creditGas(uint256 startGas, bytes32 commitmentHash) internal { | ||
| uint256 gasUsed = startGas - gasleft() + 21000; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is 21,000 necessary here? I assume it's only for refunding the last transfer call?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The 21000 accounts for the base transaction gas cost (intrinsic gas) that's deducted by the EVM before contract execution begins, so it's not captured by gasleft(). It's added for each transaction (submitInitial, commitPrevRandao, submitFinal) to ensure the relayer is refunded for the full transaction cost, not just the contract execution portion. Let me know what you think of this. |
||
| creditedGas[commitmentHash] += gasUsed; | ||
| emit GasCredited(msg.sender, commitmentHash, gasUsed); | ||
| } | ||
|
|
||
| /** | ||
| * @dev Calculate and send refund if progress meets threshold. | ||
| * | ||
| * Refund if progress >= refundTarget. | ||
| */ | ||
| function _refundWithProgress(uint256 startGas, uint256 previousGas, uint256 progress) internal { | ||
| if (progress < refundTarget) { | ||
| return; | ||
| } | ||
|
|
||
| uint256 currentGas = startGas - gasleft() + 21000; | ||
| uint256 totalGasUsed = currentGas + previousGas; | ||
| uint256 effectiveGasPrice = tx.gasprice < maxGasPrice ? tx.gasprice : maxGasPrice; | ||
| uint256 refundAmount = totalGasUsed * effectiveGasPrice; | ||
|
|
||
| if (refundAmount > maxRefundAmount) { | ||
| refundAmount = maxRefundAmount; | ||
| } | ||
|
|
||
| if (refundAmount > 0 && address(this).balance >= refundAmount) { | ||
| (bool success,) = payable(msg.sender).call{value: refundAmount}(""); | ||
| if (success) { | ||
| emit SubmissionRefunded(msg.sender, progress, refundAmount, totalGasUsed); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /* Admin Functions */ | ||
|
|
||
| function setMaxGasPrice(uint256 _maxGasPrice) external { | ||
| _checkOwner(); | ||
| maxGasPrice = _maxGasPrice; | ||
| } | ||
|
|
||
| function setMaxRefundAmount(uint256 _maxRefundAmount) external { | ||
| _checkOwner(); | ||
| maxRefundAmount = _maxRefundAmount; | ||
| } | ||
|
|
||
| function setRefundTarget(uint256 _refundTarget) external { | ||
| _checkOwner(); | ||
| refundTarget = _refundTarget; | ||
| } | ||
|
|
||
| function withdrawFunds(address payable recipient, uint256 amount) external { | ||
| _checkOwner(); | ||
| if (recipient == address(0)) { | ||
| revert InvalidAddress(); | ||
| } | ||
|
|
||
| (bool success,) = recipient.call{value: amount}(""); | ||
| if (!success) { | ||
| revert TransferFailed(); | ||
| } | ||
|
|
||
| emit FundsWithdrawn(recipient, amount); | ||
| } | ||
|
|
||
| function transferOwnership(address newOwner) external { | ||
| _checkOwner(); | ||
| if (newOwner == address(0)) { | ||
| revert InvalidAddress(); | ||
| } | ||
| owner = newOwner; | ||
| } | ||
|
|
||
| /* View Functions */ | ||
|
|
||
| /** | ||
| * @dev Calculate expected refund for a given progress. | ||
| * Useful for relayers to estimate payouts before submitting. | ||
| */ | ||
| function estimatePayout(uint256 gasUsed, uint256 gasPrice, uint256 progress) | ||
| external | ||
| view | ||
| returns (uint256 refundAmount) | ||
| { | ||
| if (progress < refundTarget) { | ||
| return 0; | ||
| } | ||
|
|
||
| uint256 effectiveGasPrice = gasPrice < maxGasPrice ? gasPrice : maxGasPrice; | ||
| refundAmount = gasUsed * effectiveGasPrice; | ||
|
|
||
| if (refundAmount > maxRefundAmount) { | ||
| refundAmount = maxRefundAmount; | ||
| } | ||
| } | ||
|
|
||
| receive() external payable { | ||
| emit FundsDeposited(msg.sender, msg.value); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Who is the owner? Does our team own this key?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, could be our SAFE multisig.