Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1668 +/- ##
==========================================
+ Coverage 81.20% 82.83% +1.63%
==========================================
Files 22 23 +1
Lines 1016 1113 +97
Branches 196 213 +17
==========================================
+ Hits 825 922 +97
Misses 174 174
Partials 17 17
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
| 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()) } | ||
| } | ||
| } |
contracts/src/BeefyClientWrapper.sol
Outdated
| ticketOwner[commitmentHash] = msg.sender; | ||
| activeTicket[msg.sender] = commitmentHash; | ||
|
|
||
| _refundGas(startGas); |
There was a problem hiding this comment.
I’d suggest crediting the gas cost without refunding it immediately, and only issuing the refund after submitFinal.
|
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 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. |
contracts/src/BeefyClientWrapper.sol
Outdated
| 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); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
|
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). |
|
@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? |
contracts/src/BeefyClientWrapper.sol
Outdated
| * @dev Forwards BeefyClient submissions and refunds gas costs to whitelisted relayers. | ||
| * Implements soft round-robin scheduling to prevent competition. | ||
| */ | ||
| contract BeefyClientWrapper is IInitializable, IUpgradable { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
Who is the owner? Does our team own this key?
There was a problem hiding this comment.
Yeah, could be our SAFE multisig.
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. |
Agreed. If the goal is simply to have multiple BEEFY relayers cooperate to update the light client, then on-chain or off-chain round-robin (RR) coordination may not be necessary. As we’ve observed with the Flashbots RPC, multiple relayers can submit updates concurrently, and only one will succeed—the others will fail without incurring any fees. This effectively provides a form of round-robin behavior at the RPC level. |
contracts/src/BeefyClientWrapper.sol
Outdated
| function createFinalBitfield(bytes32 commitmentHash, uint256[] calldata bitfield) | ||
| external | ||
| view | ||
| returns (uint256[] memory) | ||
| { | ||
| return beefyClient.createFinalBitfield(commitmentHash, bitfield); | ||
| } | ||
|
|
||
| function latestBeefyBlock() external view returns (uint64) { | ||
| return beefyClient.latestBeefyBlock(); | ||
| } | ||
|
|
||
| function createInitialBitfield(uint256[] calldata bitsToSet, uint256 length) | ||
| external | ||
| view | ||
| returns (uint256[] memory) | ||
| { | ||
| return beefyClient.createInitialBitfield(bitsToSet, length); | ||
| } |
There was a problem hiding this comment.
It seems these view functions are simply forwarding, and I'm not sure they're necessary, as the relayer can directly retrieve states from the BeefyClient.
| } | ||
|
|
||
| function _creditGas(uint256 startGas, bytes32 commitmentHash) internal { | ||
| uint256 gasUsed = startGas - gasleft() + 21000; |
There was a problem hiding this comment.
Is 21,000 necessary here? I assume it's only for refunding the last transfer call?
There was a problem hiding this comment.
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.
Resolves: SNO-1668