Skip to content

Commit 74fd0ea

Browse files
authored
Merge pull request #1320 from keep-network/remove-operator-trust-service-contract
Operator callback reimbursment in the operator contract
2 parents 432889a + b2980ff commit 74fd0ea

11 files changed

+707
-144
lines changed

contracts/solidity/contracts/KeepRandomBeaconOperator.sol

Lines changed: 66 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import "./utils/AddressArrayUtils.sol";
88
import "./libraries/operator/GroupSelection.sol";
99
import "./libraries/operator/Groups.sol";
1010
import "./libraries/operator/DKGResultVerification.sol";
11+
import "./libraries/operator/Reimbursements.sol";
1112

1213
interface ServiceContract {
1314
function entryCreated(uint256 requestId, bytes calldata entry, address payable submitter) external;
@@ -95,8 +96,9 @@ contract KeepRandomBeaconOperator is ReentrancyGuard {
9596
uint256 public relayEntryTimeout = relayEntryGenerationTime.add(groupSize.mul(resultPublicationBlockStep));
9697

9798
// Gas required to verify BLS signature and produce successful relay
98-
// entry. Excludes callback and DKG gas.
99-
uint256 public entryVerificationGasEstimate = 240000;
99+
// entry. Excludes callback and DKG gas. The worst case (most expensive)
100+
// scenario.
101+
uint256 public entryVerificationGasEstimate = 280000;
100102

101103
// Gas required to submit DKG result. Excludes initiation of group selection.
102104
uint256 public dkgGasEstimate = 1740000;
@@ -118,6 +120,7 @@ contract KeepRandomBeaconOperator is ReentrancyGuard {
118120
struct SigningRequest {
119121
uint256 relayRequestId;
120122
uint256 entryVerificationAndProfitFee;
123+
uint256 callbackFee;
121124
uint256 groupIndex;
122125
bytes previousEntry;
123126
address serviceContract;
@@ -371,17 +374,15 @@ contract KeepRandomBeaconOperator is ReentrancyGuard {
371374
uint256 surplus = dkgSubmitterReimbursementFee.sub(reimbursementFee);
372375
dkgSubmitterReimbursementFee = 0;
373376
// Reimburse submitter with actual DKG cost.
374-
(bool success, ) = magpie.call.value(reimbursementFee)("");
375-
require(success, "Failed reimburse actual DKG cost");
377+
magpie.call.value(reimbursementFee)("");
376378

377379
// Return surplus to the contract that started DKG.
378380
groupSelectionStarterContract.fundDkgFeePool.value(surplus)();
379381
} else {
380382
// If submitter used higher gas price reimburse only dkgSubmitterReimbursementFee max.
381383
reimbursementFee = dkgSubmitterReimbursementFee;
382384
dkgSubmitterReimbursementFee = 0;
383-
(bool success, ) = magpie.call.value(reimbursementFee)("");
384-
require(success, "Failed reimburse DKG fee");
385+
magpie.call.value(reimbursementFee)("");
385386
}
386387
}
387388

@@ -403,18 +404,26 @@ contract KeepRandomBeaconOperator is ReentrancyGuard {
403404
uint256 requestId,
404405
bytes memory previousEntry
405406
) public payable onlyServiceContract {
407+
uint256 entryVerificationAndProfitFee = groupProfitFee().add(
408+
entryVerificationGasEstimate.mul(gasPriceWithFluctuationMargin(priceFeedEstimate))
409+
);
406410
require(
407-
msg.value >= groupProfitFee().add(entryVerificationGasEstimate.mul(gasPriceWithFluctuationMargin(priceFeedEstimate))),
411+
msg.value >= entryVerificationAndProfitFee,
408412
"Insufficient new entry fee"
409413
);
410-
signRelayEntry(requestId, previousEntry, msg.sender, msg.value);
414+
uint256 callbackFee = msg.value.sub(entryVerificationAndProfitFee);
415+
signRelayEntry(
416+
requestId, previousEntry, msg.sender,
417+
entryVerificationAndProfitFee, callbackFee
418+
);
411419
}
412420

413421
function signRelayEntry(
414422
uint256 requestId,
415423
bytes memory previousEntry,
416424
address serviceContract,
417-
uint256 entryVerificationAndProfitFee
425+
uint256 entryVerificationAndProfitFee,
426+
uint256 callbackFee
418427
) internal {
419428
require(!isEntryInProgress() || hasEntryTimedOut(), "Beacon is busy");
420429

@@ -424,6 +433,7 @@ contract KeepRandomBeaconOperator is ReentrancyGuard {
424433
signingRequest = SigningRequest(
425434
requestId,
426435
entryVerificationAndProfitFee,
436+
callbackFee,
427437
groupIndex,
428438
previousEntry,
429439
serviceContract
@@ -455,25 +465,60 @@ contract KeepRandomBeaconOperator is ReentrancyGuard {
455465

456466
emit RelayEntrySubmitted();
457467

458-
ServiceContract(signingRequest.serviceContract).entryCreated(
459-
signingRequest.relayRequestId,
460-
_groupSignature,
461-
msg.sender
468+
// Spend no more than groupSelectionGasEstimate + 40000 gas max
469+
// This will prevent relayEntry failure in case the service contract is compromised
470+
signingRequest.serviceContract.call.gas(groupSelectionGasEstimate.add(40000))(
471+
abi.encodeWithSignature(
472+
"entryCreated(uint256,bytes,address)",
473+
signingRequest.relayRequestId,
474+
_groupSignature,
475+
msg.sender
476+
)
462477
);
463478

479+
if (signingRequest.callbackFee > 0) {
480+
executeCallback(signingRequest, uint256(keccak256(_groupSignature)));
481+
}
482+
464483
(uint256 groupMemberReward, uint256 submitterReward, uint256 subsidy) = newEntryRewardsBreakdown();
465484
groups.addGroupMemberReward(groupPubKey, groupMemberReward);
466485

467-
(bool success, ) = stakingContract.magpieOf(msg.sender).call.value(submitterReward)("");
468-
require(success, "Failed send relay submitter reward");
486+
stakingContract.magpieOf(msg.sender).call.value(submitterReward)("");
469487

470488
if (subsidy > 0) {
471-
ServiceContract(signingRequest.serviceContract).fundRequestSubsidyFeePool.value(subsidy)();
489+
signingRequest.serviceContract.call.gas(35000).value(subsidy)(abi.encodeWithSignature("fundRequestSubsidyFeePool()"));
472490
}
473491

474492
currentEntryStartBlock = 0;
475493
}
476494

495+
/**
496+
* @dev Executes customer specified callback for the relay entry request.
497+
* @param signingRequest Request data tracked internally by this contract.
498+
* @param entry The generated random number.
499+
*/
500+
function executeCallback(SigningRequest memory signingRequest, uint256 entry) internal {
501+
uint256 callbackFee = signingRequest.callbackFee;
502+
503+
// Make sure not to spend more than what was received from the service contract for the callback
504+
uint256 gasLimit = callbackFee.div(gasPriceWithFluctuationMargin(priceFeedEstimate));
505+
506+
bytes memory callbackReturnData;
507+
uint256 gasBeforeCallback = gasleft();
508+
(, callbackReturnData) = signingRequest.serviceContract.call.gas(gasLimit)(abi.encodeWithSignature("executeCallback(uint256,uint256)", signingRequest.relayRequestId, entry));
509+
uint256 gasAfterCallback = gasleft();
510+
uint256 gasSpent = gasBeforeCallback.sub(gasAfterCallback);
511+
512+
Reimbursements.reimburseCallback(
513+
stakingContract,
514+
priceFeedEstimate,
515+
gasLimit,
516+
gasSpent,
517+
callbackFee,
518+
callbackReturnData
519+
);
520+
}
521+
477522
/**
478523
* @dev Get rewards breakdown in wei for successful entry for the current signing request.
479524
*/
@@ -574,7 +619,8 @@ contract KeepRandomBeaconOperator is ReentrancyGuard {
574619
signingRequest.relayRequestId,
575620
signingRequest.previousEntry,
576621
signingRequest.serviceContract,
577-
signingRequest.entryVerificationAndProfitFee
622+
signingRequest.entryVerificationAndProfitFee,
623+
signingRequest.callbackFee
578624
);
579625
}
580626
}
@@ -659,8 +705,9 @@ contract KeepRandomBeaconOperator is ReentrancyGuard {
659705
function withdrawGroupMemberRewards(address operator, uint256 groupIndex, uint256[] memory groupMemberIndices) public nonReentrant {
660706
uint256 accumulatedRewards = groups.withdrawFromGroup(operator, groupIndex, groupMemberIndices);
661707
(bool success, ) = stakingContract.magpieOf(operator).call.value(accumulatedRewards)("");
662-
require(success, "Failed withdraw rewards");
663-
emit GroupMemberRewardsWithdrawn(stakingContract.magpieOf(operator), operator, accumulatedRewards, groupIndex);
708+
if (success) {
709+
emit GroupMemberRewardsWithdrawn(stakingContract.magpieOf(operator), operator, accumulatedRewards, groupIndex);
710+
}
664711
}
665712

666713
/**

contracts/solidity/contracts/KeepRandomBeaconServiceImplV1.sol

Lines changed: 32 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ contract KeepRandomBeaconServiceImplV1 is DelayedWithdrawal, ReentrancyGuard {
6666

6767
bytes internal _previousEntry;
6868

69+
// Cost of executing executeCallback() on this contract, require checks and .call
70+
uint256 internal _baseCallbackGas;
71+
6972
struct Callback {
7073
address callbackContract;
7174
string callbackMethod;
@@ -130,6 +133,7 @@ contract KeepRandomBeaconServiceImplV1 is DelayedWithdrawal, ReentrancyGuard {
130133
_pendingWithdrawal = 0;
131134
_previousEntry = _beaconSeed;
132135
_registry = registry;
136+
_baseCallbackGas = 18845;
133137
}
134138

135139
/**
@@ -261,7 +265,7 @@ contract KeepRandomBeaconServiceImplV1 is DelayedWithdrawal, ReentrancyGuard {
261265
uint256 requestId = _requestCounter;
262266

263267
operatorContract.sign.value(
264-
selectedOperatorContractFee
268+
selectedOperatorContractFee.add(callbackFee)
265269
)(requestId, _previousEntry);
266270

267271
// If selected operator contract is cheaper than expected return the
@@ -304,59 +308,30 @@ contract KeepRandomBeaconServiceImplV1 is DelayedWithdrawal, ReentrancyGuard {
304308
uint256 entryAsNumber = uint256(keccak256(entry));
305309
emit RelayEntryGenerated(requestId, entryAsNumber);
306310

307-
if (_callbacks[requestId].callbackContract != address(0)) {
308-
executeEntryCreatedCallback(requestId, entryAsNumber, submitter);
309-
delete _callbacks[requestId];
310-
}
311-
312311
createGroupIfApplicable(entryAsNumber, submitter);
313312
}
314313

315314
/**
316315
* @dev Executes customer specified callback for the relay entry request.
317316
* @param requestId Request id tracked internally by this contract.
318317
* @param entry The generated random number.
319-
* @param submitter Relay entry submitter.
318+
* @return Address to receive callback surplus.
320319
*/
321-
function executeEntryCreatedCallback(uint256 requestId, uint256 entry, address payable submitter) internal {
322-
bool success; // Store status of external contract call.
323-
bytes memory data; // Store result data of external contract call.
324-
325-
uint256 gasBeforeCallback = gasleft();
326-
(success, data) = _callbacks[requestId].callbackContract.call.gas(
327-
_callbacks[requestId].callbackGas
328-
)(abi.encodeWithSignature(_callbacks[requestId].callbackMethod, entry));
329-
uint256 gasSpent = gasBeforeCallback.sub(gasleft()).add(21000); // Also reimburse 21000 gas (ethereum transaction minimum gas)
330-
331-
uint256 gasPrice = _priceFeedEstimate;
332-
// We need to check if tx.gasprice is non-zero as a workaround to a bug
333-
// in go-ethereum:
334-
// https://github.com/ethereum/go-ethereum/pull/20189
335-
if (tx.gasprice > 0 && tx.gasprice < _priceFeedEstimate) {
336-
gasPrice = tx.gasprice;
337-
}
320+
function executeCallback(uint256 requestId, uint256 entry) public returns (address payable surplusRecipient) {
321+
require(
322+
_operatorContracts.contains(msg.sender),
323+
"Only authorized operator contract can call execute callback."
324+
);
338325

339-
// Obtain the actual callback gas expenditure and refund the surplus.
340-
uint256 callbackSurplus = 0;
341-
uint256 callbackFee = gasSpent.mul(gasPrice);
342-
343-
// If we spent less on the callback than the customer transferred for the
344-
// callback execution, we need to reimburse the difference.
345-
if (callbackFee < _callbacks[requestId].callbackFee) {
346-
callbackSurplus = _callbacks[requestId].callbackFee.sub(callbackFee);
347-
// Reimburse submitter with his actual callback cost.
348-
(success, ) = submitter.call.value(callbackFee)("");
349-
require(success, "Failed reimburse actual callback cost");
350-
351-
// Return callback surplus to the requestor.
352-
(success, ) = _callbacks[requestId].surplusRecipient.call.value(callbackSurplus)("");
353-
require(success, "Failed send callback surplus");
354-
355-
} else {
356-
// Reimburse submitter with the callback payment sent by the requestor.
357-
(success, ) = submitter.call.value(_callbacks[requestId].callbackFee)("");
358-
require(success, "Failed reimburse callback payment");
359-
}
326+
require(
327+
_callbacks[requestId].callbackContract != address(0),
328+
"Callback contract not found"
329+
);
330+
331+
_callbacks[requestId].callbackContract.call(abi.encodeWithSignature(_callbacks[requestId].callbackMethod, entry));
332+
333+
surplusRecipient = _callbacks[requestId].surplusRecipient;
334+
delete _callbacks[requestId];
360335
}
361336

362337
/**
@@ -377,6 +352,13 @@ contract KeepRandomBeaconServiceImplV1 is DelayedWithdrawal, ReentrancyGuard {
377352
}
378353
}
379354

355+
/**
356+
* @dev Get base callback gas required for relay entry callback.
357+
*/
358+
function baseCallbackGas() public view returns(uint256) {
359+
return _baseCallbackGas;
360+
}
361+
380362
/**
381363
* @dev Set the gas price in wei for estimating relay entry request payment.
382364
* @param priceFeedEstimate is the gas price required for estimating relay entry request payment.
@@ -407,9 +389,12 @@ contract KeepRandomBeaconServiceImplV1 is DelayedWithdrawal, ReentrancyGuard {
407389
/**
408390
* @dev Get the minimum payment in wei for relay entry callback.
409391
* The returned value includes safety margin for gas price fluctuations.
410-
* @param callbackGas Gas required for the callback.
392+
* @param _callbackGas Gas required for the callback.
411393
*/
412-
function callbackFee(uint256 callbackGas) public view returns(uint256) {
394+
function callbackFee(uint256 _callbackGas) public view returns(uint256) {
395+
// gas for the callback itself plus additional operational costs of
396+
// executing the callback
397+
uint256 callbackGas = _callbackGas == 0 ? 0 : _callbackGas.add(_baseCallbackGas);
413398
// We take the gas price from the price feed to not let malicious
414399
// miner-requestors manipulate the gas price when requesting relay entry
415400
// and underpricing expensive callbacks.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
pragma solidity ^0.5.4;
2+
3+
import "openzeppelin-solidity/contracts/math/SafeMath.sol";
4+
import "solidity-bytes-utils/contracts/BytesLib.sol";
5+
import "../../TokenStaking.sol";
6+
7+
library Reimbursements {
8+
using SafeMath for uint256;
9+
using BytesLib for bytes;
10+
11+
/**
12+
* @dev Reimburses callback execution cost and surplus based on actual gas
13+
* usage to the submitter's beneficiary address and if necessary to the
14+
* callback requestor (surplus recipient).
15+
* @param stakingContract Staking contract to get the address of the beneficiary
16+
* @param gasLimit Gas limit set for the callback
17+
* @param gasSpent Gas spent by the submitter on the callback
18+
* @param callbackFee Fee paid for the callback by the requestor
19+
* @param callbackReturnData Data containing surplus recipient address
20+
*/
21+
function reimburseCallback(
22+
TokenStaking stakingContract,
23+
uint256 priceFeedEstimate,
24+
uint256 gasLimit,
25+
uint256 gasSpent,
26+
uint256 callbackFee,
27+
bytes memory callbackReturnData
28+
) public {
29+
uint256 gasPrice = tx.gasprice < priceFeedEstimate ? tx.gasprice : priceFeedEstimate;
30+
31+
// Obtain the actual callback gas expenditure and refund the surplus.
32+
//
33+
// In case of heavily underpriced transactions, EVM may wrap the call
34+
// with additional opcodes. In this case gasSpent > gasLimit.
35+
// The worst scenario cost is included in entry verification fee.
36+
// If this happens we return just the gasLimit here.
37+
uint256 actualCallbackGas = gasSpent < gasLimit ? gasSpent : gasLimit;
38+
uint256 actualCallbackFee = actualCallbackGas.mul(gasPrice);
39+
40+
// Get the beneficiary.
41+
address payable magpie = stakingContract.magpieOf(msg.sender);
42+
43+
// If we spent less on the callback than the customer transferred for the
44+
// callback execution, we need to reimburse the difference.
45+
if (actualCallbackFee < callbackFee) {
46+
uint256 callbackSurplus = callbackFee.sub(actualCallbackFee);
47+
// Reimburse submitter with his actual callback cost.
48+
magpie.call.value(actualCallbackFee)("");
49+
50+
// Return callback surplus to the requestor.
51+
// Expecting 32 bytes data containing 20 byte address
52+
if (callbackReturnData.length == 32) {
53+
address surplusRecipient = callbackReturnData.toAddress(12);
54+
surplusRecipient.call.gas(8000).value(callbackSurplus)("");
55+
}
56+
} else {
57+
// Reimburse submitter with the callback payment sent by the requestor.
58+
magpie.call.value(callbackFee)("");
59+
}
60+
}
61+
}

0 commit comments

Comments
 (0)