Skip to content

Conversation

@claravanstaden
Copy link
Contributor

@claravanstaden claravanstaden commented Jan 7, 2026

Resolves: SNO-1668

@codecov
Copy link

codecov bot commented Jan 7, 2026

Codecov Report

❌ Patch coverage is 86.59794% with 26 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.27%. Comparing base (1cf39a2) to head (39bf3fc).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
contracts/src/BeefyClientWrapper.sol 85.55% 26 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1668      +/-   ##
==========================================
- Coverage   91.02%   90.27%   -0.76%     
==========================================
  Files          19       21       +2     
  Lines         903     1100     +197     
  Branches      164      192      +28     
==========================================
+ Hits          822      993     +171     
- Misses         64       90      +26     
  Partials       17       17              
Flag Coverage Δ
solidity 90.27% <86.59%> (-0.76%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment on lines 42 to 50
address implementation = ERC1967.load();
assembly {
let result := delegatecall(gas(), implementation, 0, 0, 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems unnecessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 919f503

ticketOwner[commitmentHash] = msg.sender;
activeTicket[msg.sender] = commitmentHash;

_refundGas(startGas);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’d suggest crediting the gas cost without refunding it immediately, and only issuing the refund after submitFinal.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, addressed in 5828412.

@yrong
Copy link
Contributor

yrong commented Jan 8, 2026

Cool. I assume that initially we would top up the wrapper contract ourselves, and that we would only refund the BEEFY relayer for the transaction cost of the consensus update.

We may also want to allow end users to tip for a message that is included in a relay-chain block but has not yet been finalized by BEEFY.

The tip storage could be a map from relayBlockNumber → tipFee. BEEFY relayers would monitor both this tip storage and the Beefy finality updates. Once a BEEFY commitment advances past the corresponding relayBlockNumber—making the message verifiable—they would compete to submit the consensus update and claim the tip.

I assume this would be the main incentive mechanism for the consensus relay: relayers receive additional rewards from end users only when there is a message to relay.

Comment on lines 185 to 196
function _refundGas(uint256 startGas) internal {
uint256 gasUsed = startGas - gasleft() + 21000;
uint256 effectiveGasPrice = tx.gasprice < maxGasPrice ? tx.gasprice : maxGasPrice;
uint256 refundAmount = gasUsed * effectiveGasPrice;

if (address(this).balance >= refundAmount) {
(bool success,) = payable(msg.sender).call{value: refundAmount}("");
if (success) {
emit SubmissionRefunded(msg.sender, refundAmount, gasUsed);
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function could be exploitable. If msg.sender is a malicious contract with a receive() or fallback() function that consumes a large amount of gas, it could potentially drain the proxy contract.

I’d suggest adding a hard cap on the refundAmount to mitigate this risk.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! e43ab5f

@claravanstaden
Copy link
Contributor Author

We may also want to allow end users to tip for a message that is included in a relay-chain block but has not yet been finalized by BEEFY.

@yrong good idea! Added in 39bf3fc, let me know what you think.

@alistair-singh
Copy link
Contributor

I think this can be done way simpler. You just need a refund target, which is a number in blocks, say 300 blocks (30 minutes). You always refund, but scale it according to how much progress the relayer made. If a relayer chooses to relay every 30 blocks, they will only get 10% of gas returned as refund. If a relayer waits 300 blocks or more, it gets 100% of its gas as refund. There is no round robin, relayers must monitor open tickets and see if they will get a refund based on the pending tickets. If there is a race condition where two relayers compete, this scaled refund makes sure each one gets paid a portion based on progress made. Remember relayers can abandon tickets if there is a race condition where another relayer competes with them.

This same idea can be extended to rewards/tips. You could choose a reward target, which is a number in blocks, say 2400 (4 hours). You refund as per the above logic until 300 block refund target, for every block over, up untill 2400 you scale by amount of blocks progressed and pay a portion of the reward. So if a relayer makes 600 blocks progress, you give them a full refund for meeting the refund target (300/300), and then a 12.5% portion of the reward (300/2400).

@claravanstaden
Copy link
Contributor Author

@alistair-singh this sounds cool, and is simpler. Relayers might frequently lose gas spent on submitInitial, if more than one relayer submits at the same time. What do you think about this? And do we remove the relayer whitelist then?

* @dev Forwards BeefyClient submissions and refunds gas costs to whitelisted relayers.
* Implements soft round-robin scheduling to prevent competition.
*/
contract BeefyClientWrapper is IInitializable, IUpgradable {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why we need proxy pattern and upgradable here. We may need it, but we can get away with pointing the wrapper at the Gateway contract instead of the Beefy contract. It can read the current beefy client from the Gateway. This means when we upgrade the Beefy client on the gateway, the wrapper is automatically picks up the new Beefy Instance.

Since this is a wrapper we should also strive to rather throw it away and deploy a new wrapper if we ever have to upgrade it, avoiding forms of governance if possible. We would only really need to migrate any Eth held by the contract. So Proxy Pattern seems to heavy for this requirement.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't have a proxy pattern at first, but because this contract holds state I figured it would be better, esp if we want to change some of the config. I don't have a strong opinion about this either way.

error AlreadyInitialized();
error TicketAlreadyActive();

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.

@alistair-singh
Copy link
Contributor

alistair-singh commented Jan 8, 2026

@alistair-singh this sounds cool, and is simpler. Relayers might frequently lose gas spent on submitInitial, if more than one relayer submits at the same time. What do you think about this? And do we remove the relayer whitelist then?

I think it might happen occasionally, but we need some system where we can tune the parameters to encourage all the relayers to pipeline consensus updates as opposed to use manually configuring and whitelisting relayers. I dont think it needs to be a perfect system either, we must trust relayers to act in their best interest. At worst, they will abandon the ticket and post another.

This edge case is something that can be addressed in the wrapper as well, by reverting early if some condition is not met. For example you can do something like compare and swap operation. e.g. The current latest ticket open is for block X, I want to relay block X+300, assuring my 100% refund, when I submit initial to claim a ticket via the wrapper i submit both X and X+300, and the submit initial in the wrapper can revert if X has changed onchain. So it still costs, but less than submit initial. The only time X will change onchain is if two relayers submit and it gets included in the same block. We can introduce random in the relayer to offset this in practise, so generate a random number N between 1-20, and the relay block X+300+N. So relayers never try to relay at the exact interval for example.

We could even do something like we do for message relayers and co-ordinate round robin offchain.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants