Skip to content

Commit

Permalink
fix(protocol): fix bridge quota processing and make processMessage
Browse files Browse the repository at this point in the history
…return data (#17027)

Co-authored-by: dantaik <[email protected]>
Co-authored-by: D <[email protected]>
Co-authored-by: adaki2004 <[email protected]>
  • Loading branch information
4 people authored May 9, 2024
1 parent 22ac9ae commit 277dade
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 41 deletions.
104 changes: 64 additions & 40 deletions packages/protocol/contracts/bridge/Bridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ contract Bridge is EssentialContract, IBridge {
error B_INVALID_VALUE();
error B_INSUFFICIENT_GAS();
error B_MESSAGE_NOT_SENT();
error B_OUT_OF_ETH_QUOTA();
error B_PERMISSION_DENIED();
error B_RETRY_FAILED();
error B_SIGNAL_NOT_RECEIVED();
Expand Down Expand Up @@ -186,7 +187,7 @@ contract Bridge is EssentialContract, IBridge {
);

_updateMessageStatus(msgHash, Status.RECALLED);
_consumeEtherQuota(_message.value);
if (!_consumeEtherQuota(_message.value)) revert B_OUT_OF_ETH_QUOTA();

// Execute the recall logic based on the contract's support for the
// IRecallableSender interface
Expand Down Expand Up @@ -214,66 +215,84 @@ contract Bridge is EssentialContract, IBridge {
bytes calldata _proof
)
external
sameChain(_message.destChainId)
diffChain(_message.srcChainId)
whenNotPaused
nonReentrant
returns (Status status_, StatusReason reason_)
{
uint256 gasStart = gasleft();

// same as `sameChain(_message.destChainId)` but without stack-too-deep
if (_message.destChainId != block.chainid) revert B_INVALID_CHAINID();

// same as `diffChain(_message.srcChainId)` but without stack-too-deep
if (_message.srcChainId == 0 || _message.srcChainId == block.chainid) {
revert B_INVALID_CHAINID();
}

// If the gas limit is set to zero, only the owner can process the message.
if (_message.gasLimit == 0 && msg.sender != _message.destOwner) {
revert B_PERMISSION_DENIED();
}

bytes32 msgHash = hashMessage(_message);
_checkStatus(msgHash, Status.NEW);
_consumeEtherQuota(_message.value + _message.fee);

address signalService = resolve(LibStrings.B_SIGNAL_SERVICE, false);

ProcessingStats memory stats;
stats.proofSize = uint32(_proof.length);
stats.numCacheOps =
_proveSignalReceived(signalService, msgHash, _message.srcChainId, _proof);

uint256 refundAmount;
if (_unableToInvokeMessageCall(_message, signalService)) {
// Handle special addresses that don't require actual invocation but
// mark message as DONE
refundAmount = _message.value;
_updateMessageStatus(msgHash, Status.DONE);
if (!_consumeEtherQuota(_message.value + _message.fee)) {
if (msg.sender != _message.destOwner) revert B_OUT_OF_ETH_QUOTA();
status_ = Status.RETRIABLE;
reason_ = StatusReason.OUT_OF_ETH_QUOTA;
} else {
uint256 gasLimit = msg.sender == _message.destOwner
? gasleft() // ignore _message.gasLimit
: _invocationGasLimit(_message, true);

Status status =
_invokeMessageCall(_message, msgHash, gasLimit) ? Status.DONE : Status.RETRIABLE;
_updateMessageStatus(msgHash, status);
}
uint256 refundAmount;
if (_unableToInvokeMessageCall(_message, signalService)) {
// Handle special addresses that don't require actual invocation but
// mark message as DONE
refundAmount = _message.value;
status_ = Status.DONE;
reason_ = StatusReason.INVOCATION_PROHIBITED;
} else {
uint256 gasLimit = msg.sender == _message.destOwner
? gasleft() // ignore _message.gasLimit
: _invocationGasLimit(_message, true);

if (_invokeMessageCall(_message, msgHash, gasLimit)) {
status_ = Status.DONE;
reason_ = StatusReason.INVOCATION_OK;
} else {
status_ = Status.RETRIABLE;
reason_ = StatusReason.INVOCATION_FAILED;
}
}

if (_message.fee != 0) {
refundAmount += _message.fee;

if (msg.sender != _message.destOwner && _message.gasLimit != 0) {
unchecked {
uint256 refund = stats.numCacheOps * _GAS_REFUND_PER_CACHE_OPERATION;
stats.gasUsedInFeeCalc = uint32(GAS_OVERHEAD + gasStart - gasleft());
uint256 gasCharged = refund.max(stats.gasUsedInFeeCalc) - refund;
uint256 maxFee = gasCharged * _message.fee / _message.gasLimit;
uint256 baseFee = gasCharged * block.basefee;
uint256 fee =
(baseFee >= maxFee ? maxFee : (maxFee + baseFee) >> 1).min(_message.fee);

refundAmount -= fee;
msg.sender.sendEtherAndVerify(fee, _SEND_ETHER_GAS_LIMIT);
if (_message.fee != 0) {
refundAmount += _message.fee;

if (msg.sender != _message.destOwner && _message.gasLimit != 0) {
unchecked {
uint256 refund = stats.numCacheOps * _GAS_REFUND_PER_CACHE_OPERATION;
stats.gasUsedInFeeCalc = uint32(GAS_OVERHEAD + gasStart - gasleft());
uint256 gasCharged = refund.max(stats.gasUsedInFeeCalc) - refund;
uint256 maxFee = gasCharged * _message.fee / _message.gasLimit;
uint256 baseFee = gasCharged * block.basefee;
uint256 fee =
(baseFee >= maxFee ? maxFee : (maxFee + baseFee) >> 1).min(_message.fee);

refundAmount -= fee;
msg.sender.sendEtherAndVerify(fee, _SEND_ETHER_GAS_LIMIT);
}
}
}
}

_message.destOwner.sendEtherAndVerify(refundAmount, _SEND_ETHER_GAS_LIMIT);
_message.destOwner.sendEtherAndVerify(refundAmount, _SEND_ETHER_GAS_LIMIT);
}

stats.proofSize = uint32(_proof.length);
_updateMessageStatus(msgHash, status_);
emit MessageProcessed(msgHash, _message, stats);
}

Expand All @@ -290,7 +309,8 @@ contract Bridge is EssentialContract, IBridge {
{
bytes32 msgHash = hashMessage(_message);
_checkStatus(msgHash, Status.RETRIABLE);
_consumeEtherQuota(_message.value);

if (!_consumeEtherQuota(_message.value)) revert B_OUT_OF_ETH_QUOTA();

uint256 invocationGasLimit;
if (msg.sender != _message.destOwner) {
Expand Down Expand Up @@ -597,10 +617,14 @@ contract Bridge is EssentialContract, IBridge {
if (messageStatus[_msgHash] != _expectedStatus) revert B_INVALID_STATUS();
}

function _consumeEtherQuota(uint256 _amount) private {
function _consumeEtherQuota(uint256 _amount) private returns (bool) {
address quotaManager = resolve(LibStrings.B_QUOTA_MANAGER, true);
if (quotaManager != address(0)) {
IQuotaManager(quotaManager).consumeQuota(address(0), _amount);
if (quotaManager == address(0)) return true;

try IQuotaManager(quotaManager).consumeQuota(address(0), _amount) {
return true;
} catch {
return false;
}
}

Expand Down
15 changes: 14 additions & 1 deletion packages/protocol/contracts/bridge/IBridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ interface IBridge {
RECALLED
}

enum StatusReason {
INVOCATION_OK,
INVOCATION_PROHIBITED,
INVOCATION_FAILED,
OUT_OF_ETH_QUOTA
}

struct Message {
// Message ID whose value is automatically assigned.
uint64 id;
Expand Down Expand Up @@ -90,7 +97,13 @@ interface IBridge {
/// needed.
/// @param _message The message to be processed.
/// @param _proof The merkle inclusion proof.
function processMessage(Message calldata _message, bytes calldata _proof) external;
/// @return The message's status after processing and the reason for the change.
function processMessage(
Message calldata _message,
bytes calldata _proof
)
external
returns (Status, StatusReason);

/// @notice Retries to invoke the messageCall after releasing associated
/// Ether and tokens.
Expand Down
43 changes: 43 additions & 0 deletions packages/protocol/test/bridge/Bridge2_processMessage.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ contract Target is IMessageInvocable {
receive() external payable { }
}

contract OutOfQuotaManager is IQuotaManager {
function consumeQuota(address, uint256) external pure {
revert("out of quota");
}
}

contract BridgeTest2_processMessage is BridgeTest2 {
function test_bridge2_processMessage_basic() public dealEther(Alice) assertSameTotalBalance {
vm.startPrank(Alice);
Expand Down Expand Up @@ -381,4 +387,41 @@ contract BridgeTest2_processMessage is BridgeTest2 {
uint256 totalBalance2 = getBalanceForAccounts() + address(target).balance;
assertEq(totalBalance2, totalBalance);
}

function test_bridge2_processMessage__no_ether_quota()
public
dealEther(Bob)
dealEther(Alice)
assertSameTotalBalance
{
vm.startPrank(owner);
addressManager.setAddress(
uint64(block.chainid), "quota_manager", address(new OutOfQuotaManager())
);
vm.stopPrank();

IBridge.Message memory message;

message.destChainId = uint64(block.chainid);
message.srcChainId = remoteChainId;

message.gasLimit = 1_000_000;
message.fee = 5_000_000;
message.value = 2 ether;
message.destOwner = Alice;
message.to = David;

uint256 davidBalance = David.balance;

vm.prank(Bob);
vm.expectRevert(Bridge.B_OUT_OF_ETH_QUOTA.selector);
bridge.processMessage(message, fakeProof);

vm.prank(Alice);
bridge.processMessage(message, fakeProof);
bytes32 hash = bridge.hashMessage(message);
assertTrue(bridge.messageStatus(hash) == IBridge.Status.RETRIABLE);

assertEq(davidBalance, David.balance);
}
}

0 comments on commit 277dade

Please sign in to comment.