Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions contracts/scripts/DeployBeefyClientWrapper.sol
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();
}
}
15 changes: 14 additions & 1 deletion contracts/scripts/DeployLocal.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pragma solidity 0.8.33;
import {WETH9} from "canonical-weth/WETH9.sol";
import {Script} from "forge-std/Script.sol";
import {BeefyClient} from "../src/BeefyClient.sol";
import {BeefyClientWrapper} from "../src/BeefyClientWrapper.sol";
import {IGatewayV1} from "../src/v1/IGateway.sol";
import {GatewayProxy} from "../src/GatewayProxy.sol";
import {Gateway} from "../src/Gateway.sol";
Expand Down Expand Up @@ -61,6 +62,18 @@ contract DeployLocal is Script {
next
);

// Deploy BeefyClientWrapper
BeefyClientWrapper beefyClientWrapper = new BeefyClientWrapper(
address(beefyClient),
deployer,
vm.envUint("BEEFY_WRAPPER_MAX_GAS_PRICE"),
vm.envUint("BEEFY_WRAPPER_MAX_REFUND_AMOUNT"),
vm.envUint("BEEFY_WRAPPER_REFUND_TARGET")
);

// Fund wrapper for refunds
payable(address(beefyClientWrapper)).call{value: vm.envUint("BEEFY_WRAPPER_INITIAL_DEPOSIT")}("");

uint8 foreignTokenDecimals = uint8(vm.envUint("FOREIGN_TOKEN_DECIMALS"));
uint128 maxDestinationFee = uint128(vm.envUint("RESERVE_TRANSFER_MAX_DESTINATION_FEE"));

Expand Down Expand Up @@ -94,7 +107,7 @@ contract DeployLocal is Script {
// For testing call contract
new HelloWorld();

// Deploy test token for registration testing
// Deploy test token for registration testing
new Token("Test Token", "TEST", 18);

// Fund the gateway proxy contract. Used to reward relayers
Expand Down
8 changes: 8 additions & 0 deletions contracts/src/BeefyClient.sol
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,14 @@ contract BeefyClient {
return Bitfield.createBitfield(bitsToSet, length);
}

/**
* @dev Compute the hash of a commitment
* @param commitment the commitment to hash
*/
function computeCommitmentHash(Commitment calldata commitment) external pure returns (bytes32) {
return keccak256(encodeCommitment(commitment));
}

/**
* @dev Helper to create a final bitfield, with subsampled validator selections
* @param commitmentHash contains the commitmentHash signed by the validators
Expand Down
267 changes: 267 additions & 0 deletions contracts/src/BeefyClientWrapper.sol
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;
Copy link
Contributor

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?

Copy link
Contributor Author

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.

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;
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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);
}
}
Loading
Loading