Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(protocol): add an invocation delay #15553

Merged
merged 4 commits into from
Jan 24, 2024
Merged
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
216 changes: 122 additions & 94 deletions packages/protocol/contracts/bridge/Bridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,24 @@ contract Bridge is EssentialContract, IBridge {
NEW,
RETRIABLE,
DONE,
FAILED
FAILED,
RECALLED
}

uint256 internal constant PLACEHOLDER = type(uint256).max;

uint128 public nextMessageId; // slot 1
mapping(bytes32 msgHash => bool recalled) public isMessageRecalled;
mapping(bytes32 msgHash => bool recalled) private __isMessageRecalled; // deprecated
mapping(bytes32 msgHash => Status) public messageStatus; // slot 3
Context private _ctx; // // slot 4,5,6pnpm
uint256[44] private __gap;
Context private _ctx; // // slot 4,5,6
mapping(bytes32 msgHash => uint64) public messageReceivedAt;
uint256[43] private __gap;

event SignalSent(address indexed sender, bytes32 msgHash);
event MessageSent(bytes32 indexed msgHash, Message message);
event MessageReceived(bytes32 indexed msgHash, Message message, bool isRecall);
event MessageRecalled(bytes32 indexed msgHash);
event MessageExecuted(bytes32 indexed msgHash);
event DestChainEnabled(uint64 indexed chainId, bool enabled);
event MessageStatusChanged(bytes32 indexed msgHash, Status status);

Expand All @@ -60,6 +64,7 @@ contract Bridge is EssentialContract, IBridge {
error B_PERMISSION_DENIED();
error B_RECALLED_ALREADY();
error B_STATUS_MISMATCH();
error B_INVOCATION_TOO_EARLY();

modifier sameChain(uint64 chainId) {
if (chainId != block.chainid) revert B_INVALID_CHAINID();
Expand Down Expand Up @@ -131,42 +136,57 @@ contract Bridge is EssentialContract, IBridge {
sameChain(message.srcChainId)
{
bytes32 msgHash = hashMessage(message);
if (isMessageRecalled[msgHash]) revert B_RECALLED_ALREADY();
bytes32 failureSignal = signalForFailedMessage(msgHash);
if (messageStatus[failureSignal] != Status.NEW) revert B_RECALLED_ALREADY();

ISignalService signalService = ISignalService(resolve("signal_service", false));

if (!signalService.isSignalSent(address(this), msgHash)) revert B_MESSAGE_NOT_SENT();

bool received = _proveSignalReceived(
signalService, _signalForFailedMessage(msgHash), message.destChainId, proof
);
if (!received) revert B_NOT_FAILED();
bool isMessageNew = messageReceivedAt[failureSignal] == 0;
if (isMessageNew) {
ISignalService signalService = ISignalService(resolve("signal_service", false));

isMessageRecalled[msgHash] = true;
if (!signalService.isSignalSent(address(this), msgHash)) {
revert B_MESSAGE_NOT_SENT();
}

// Execute the recall logic based on the contract's support for the
// IRecallableSender interface
bool support = message.from.supportsInterface(type(IRecallableSender).interfaceId);
if (support) {
_ctx =
Context({ msgHash: msgHash, from: address(this), srcChainId: message.srcChainId });
if (!_proveSignalReceived(signalService, failureSignal, message.destChainId, proof)) {
revert B_NOT_FAILED();
}

// Perform recall
IRecallableSender(message.from).onMessageRecalled{ value: message.value }(
message, msgHash
);
messageReceivedAt[failureSignal] = uint64(block.timestamp);
}

// Reset the context after the message call
_ctx = Context({
msgHash: bytes32(PLACEHOLDER),
from: address(uint160(PLACEHOLDER)),
srcChainId: uint64(PLACEHOLDER)
});
if (block.timestamp >= getInvocationDelay() + messageReceivedAt[failureSignal]) {
delete messageReceivedAt[failureSignal];
messageStatus[failureSignal] = Status.RECALLED;

// Execute the recall logic based on the contract's support for the
// IRecallableSender interface
if (message.from.supportsInterface(type(IRecallableSender).interfaceId)) {
_ctx = Context({
msgHash: msgHash,
from: address(this),
srcChainId: message.srcChainId
});

// Perform recall
IRecallableSender(message.from).onMessageRecalled{ value: message.value }(
message, msgHash
);

// Reset the context after the message call
_ctx = Context({
msgHash: bytes32(PLACEHOLDER),
from: address(uint160(PLACEHOLDER)),
srcChainId: uint64(PLACEHOLDER)
});
} else {
message.owner.sendEther(message.value);
}
emit MessageRecalled(msgHash);
} else if (isMessageNew) {
emit MessageReceived(msgHash, message, true);
} else {
message.owner.sendEther(message.value);
revert B_INVOCATION_TOO_EARLY();
}

emit MessageRecalled(msgHash);
}

/// @notice Processes a bridge message on the destination chain. This
Expand All @@ -186,58 +206,67 @@ contract Bridge is EssentialContract, IBridge {
whenNotPaused
sameChain(message.destChainId)
{
// If the gas limit is set to zero, only the owner can process the
// message.
if (message.gasLimit == 0 && msg.sender != message.owner) {
revert B_PERMISSION_DENIED();
}

bytes32 msgHash = hashMessage(message);

if (messageStatus[msgHash] != Status.NEW) revert B_STATUS_MISMATCH();

ISignalService signalService = ISignalService(resolve("signal_service", false));
if (!_proveSignalReceived(signalService, msgHash, message.srcChainId, proof)) {
revert B_NOT_RECEIVED();
bool isMessageNew = messageReceivedAt[msgHash] == 0;

if (isMessageNew) {
if (!_proveSignalReceived(signalService, msgHash, message.srcChainId, proof)) {
revert B_NOT_RECEIVED();
}
messageReceivedAt[msgHash] = uint64(block.timestamp);
}

Status status;
uint256 refundAmount;

// Process message differently based on the target address
if (
message.to == address(0) || message.to == address(this)
|| message.to == address(signalService)
) {
// Handle special addresses that don't require actual invocation but
// mark message as DONE
status = Status.DONE;
refundAmount = message.value;
} else {
// Use the specified message gas limit if called by the owner, else
// use remaining gas
uint256 gasLimit = msg.sender == message.owner ? gasleft() : message.gasLimit;
if (block.timestamp >= getInvocationDelay() + messageReceivedAt[msgHash]) {
// If the gas limit is set to zero, only the owner can process the
// message.
if (message.gasLimit == 0 && msg.sender != message.owner) {
revert B_PERMISSION_DENIED();
}

delete messageReceivedAt[msgHash];

if (_invokeMessageCall(message, msgHash, gasLimit)) {
status = Status.DONE;
uint256 refundAmount;

// Process message differently based on the target address
if (
message.to == address(0) || message.to == address(this)
|| message.to == address(signalService)
) {
// Handle special addresses that don't require actual invocation but
// mark message as DONE
refundAmount = message.value;
_updateMessageStatus(msgHash, Status.DONE);
} else {
status = Status.RETRIABLE;
// Use the specified message gas limit if called by the owner, else
// use remaining gas
uint256 gasLimit = msg.sender == message.owner ? gasleft() : message.gasLimit;

if (_invokeMessageCall(message, msgHash, gasLimit)) {
_updateMessageStatus(msgHash, Status.DONE);
} else {
_updateMessageStatus(msgHash, Status.RETRIABLE);
}
}
}

// Update the message status
_updateMessageStatus(signalService, msgHash, status);
// Determine the refund recipient
address refundTo = message.refundTo == address(0) ? message.owner : message.refundTo;

// Determine the refund recipient
address refundTo = message.refundTo == address(0) ? message.owner : message.refundTo;

// Refund the processing fee
if (msg.sender == refundTo) {
refundTo.sendEther(message.fee + refundAmount);
// Refund the processing fee
if (msg.sender == refundTo) {
refundTo.sendEther(message.fee + refundAmount);
} else {
// If sender is another address, reward it and refund the rest
msg.sender.sendEther(message.fee);
refundTo.sendEther(refundAmount);
}
emit MessageExecuted(msgHash);
} else if (isMessageNew) {
emit MessageReceived(msgHash, message, false);
} else {
// If sender is another address, reward it and refund the rest
msg.sender.sendEther(message.fee);
refundTo.sendEther(refundAmount);
revert B_INVOCATION_TOO_EARLY();
}
}

Expand Down Expand Up @@ -272,15 +301,11 @@ contract Bridge is EssentialContract, IBridge {

// Attempt to invoke the messageCall.
if (_invokeMessageCall(message, msgHash, gasleft())) {
// Update the message status to "DONE" on successful invocation.
_updateMessageStatus(
ISignalService(resolve("signal_service", false)), msgHash, Status.DONE
);
_updateMessageStatus(msgHash, Status.DONE);
} else if (isLastAttempt) {
// Update the message status to "FAILED"
_updateMessageStatus(
ISignalService(resolve("signal_service", false)), msgHash, Status.FAILED
);
_updateMessageStatus(msgHash, Status.FAILED);
} else {
// this transaction will revert
}
}

Expand Down Expand Up @@ -311,7 +336,7 @@ contract Bridge is EssentialContract, IBridge {

return _proveSignalReceived(
ISignalService(resolve("signal_service", false)),
_signalForFailedMessage(hashMessage(message)),
signalForFailedMessage(hashMessage(message)),
message.destChainId,
proof
);
Expand Down Expand Up @@ -360,11 +385,22 @@ contract Bridge is EssentialContract, IBridge {
return _ctx;
}

/// @notice Returns the delay in seconds before a message can be executed
/// after being received.
function getInvocationDelay() public view virtual returns (uint256) {
return 0;
}

/// @notice Hash the message
function hashMessage(Message memory message) public pure returns (bytes32) {
return keccak256(abi.encode("TAIKO_MESSAGE", message));
}

/// @notice Returns a signal representing a failed/recalled message.
function signalForFailedMessage(bytes32 msgHash) public pure returns (bytes32) {
return msgHash ^ bytes32(uint256(Status.FAILED));
}

/// @notice Invokes a call message on the Bridge.
/// @param message The call message to be invoked.
/// @param msgHash The hash of the message.
Expand Down Expand Up @@ -402,20 +438,16 @@ contract Bridge is EssentialContract, IBridge {
/// mapping, the status is updated and an event is emitted.
/// @param msgHash The hash of the message.
/// @param status The new status of the message.
function _updateMessageStatus(
ISignalService signalService,
bytes32 msgHash,
Status status
)
private
{
function _updateMessageStatus(bytes32 msgHash, Status status) private {
if (messageStatus[msgHash] == status) return;

messageStatus[msgHash] = status;
emit MessageStatusChanged(msgHash, status);

if (status == Status.FAILED) {
signalService.sendSignal(_signalForFailedMessage(msgHash));
ISignalService(resolve("signal_service", false)).sendSignal(
signalForFailedMessage(msgHash)
);
}
}

Expand All @@ -442,8 +474,4 @@ contract Bridge is EssentialContract, IBridge {
proof: proof
});
}

function _signalForFailedMessage(bytes32 msgHash) private pure returns (bytes32) {
return msgHash ^ bytes32(uint256(Status.FAILED));
}
}
Loading