Skip to content
Open
Show file tree
Hide file tree
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
72 changes: 56 additions & 16 deletions src/AsyncEnabled.sol
Original file line number Diff line number Diff line change
@@ -1,48 +1,65 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {AsyncUtils} from "./AsyncUtils.sol";
import {console} from "forge-std/console.sol";
import {LocalAsyncProxy} from "./LocalAsyncProxy.sol";
import {AsyncCall, AsyncCallback} from "./AsyncUtils.sol";
import {SuperchainEnabled} from "./SuperchainEnabled.sol";
import {AsyncPromise} from "./AsyncPromise.sol";
import { IL2ToL2CrossDomainMessenger } from "@contracts-bedrock/L2/interfaces/IL2ToL2CrossDomainMessenger.sol";
import { Predeploys } from "@contracts-bedrock/libraries/Predeploys.sol";
import {IL2ToL2CrossDomainMessenger} from "@contracts-bedrock/L2/interfaces/IL2ToL2CrossDomainMessenger.sol";
import {Predeploys} from "@contracts-bedrock/libraries/Predeploys.sol";

contract AsyncEnabled is SuperchainEnabled {
// mapping of address to chainId to remote caller proxy, should probably be private
mapping(address => mapping(uint256 => LocalAsyncProxy)) public remoteAsyncProxies;

constructor() {
console.log("an asyncEnabled contract was just deployed!");
}

// gets a remote instance of the contract, creating it if it doesn't exist
function getAsyncProxy(address _remoteAddress, uint256 _remoteChainId) internal returns (address) {
if (address(remoteAsyncProxies[_remoteAddress][_remoteChainId]) == address(0)) {
remoteAsyncProxies[_remoteAddress][_remoteChainId] = new LocalAsyncProxy{salt: bytes32(0)}(_remoteAddress, _remoteChainId);
if (isProxyNotCreated(_remoteAddress, _remoteChainId)) {
createLocalAsyncProxy(_remoteAddress, _remoteChainId);
}
return address(remoteAsyncProxies[_remoteAddress][_remoteChainId]);
}

function isProxyNotCreated(address _remoteAddress, uint256 _remoteChainId) internal view returns (bool) {
return address(remoteAsyncProxies[_remoteAddress][_remoteChainId]) == address(0);
}

function createLocalAsyncProxy(address _remoteAddress, uint256 _remoteChainId) internal {
remoteAsyncProxies[_remoteAddress][_remoteChainId] = new LocalAsyncProxy{salt: bytes32(0)}(_remoteAddress, _remoteChainId);
}

function relayAsyncCall(AsyncCall calldata _asyncCall) external {
// Ensure the crossDomainSender is a valid async proxy for the remote address and chain
// TODO: other sanity checks on _asyncCall values
LocalAsyncProxy expectedCrossDomainSender = AsyncUtils.calculateLocalAsyncProxyAddress(
_asyncCall.from.addr,
address(this),
block.chainid
);
require(_isValidCrossDomainSender(address(expectedCrossDomainSender)));
require(isValidCrossDomainSender(_asyncCall), "Invalid cross-domain sender");
console.log("valid CDM, relaying async call");

(bool success, bytes memory returndata) = address(this).call(_asyncCall.data);
(bool success, bytes memory returndata) = executeAsyncCall(_asyncCall);

console.log("AsyncCallRelayer relayed, success: %s, returndata: ", success);
console.logBytes(returndata);

require(success, "Relaying async call failed");

relayCallback(_asyncCall, success, returndata);
}

function isValidCrossDomainSender(AsyncCall calldata _asyncCall) internal view returns (bool) {
LocalAsyncProxy expectedCrossDomainSender = AsyncUtils.calculateLocalAsyncProxyAddress(
_asyncCall.from.addr,
address(this),
block.chainid
);
return _isValidCrossDomainSender(address(expectedCrossDomainSender));
}

function executeAsyncCall(AsyncCall calldata _asyncCall) internal returns (bool, bytes memory) {
return address(this).call(_asyncCall.data);
}

function relayCallback(AsyncCall calldata _asyncCall, bool success, bytes memory returndata) internal {
bytes32 asyncCallId = AsyncUtils.getAsyncCallId(_asyncCall);
AsyncCallback memory callback = AsyncCallback({
asyncCallId: asyncCallId,
Expand All @@ -65,6 +82,12 @@ contract AsyncEnabled is SuperchainEnabled {
function relayAsyncCallback(AsyncCallback calldata _callback) external {
console.log("in relayAsyncCallback");

require(isValidPromiseCallbackSender(_callback), "Invalid promise callback sender");

executeCallback(_callback);
}

function isValidPromiseCallbackSender(AsyncCallback calldata _callback) internal view returns (bool) {
address crossDomainCallbackSender = IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER).crossDomainMessageSender();
uint256 crossDomainCallbackSource = IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER).crossDomainMessageSource();
// TODO
Expand All @@ -77,7 +100,11 @@ contract AsyncEnabled is SuperchainEnabled {

AsyncPromise promiseContract = remoteProxy.promisesById(_callback.asyncCallId);

require(promiseContract.remoteTarget() == crossDomainCallbackSender, "Invalid promise callback sender");
return promiseContract.remoteTarget() == crossDomainCallbackSender;
}

function executeCallback(AsyncCallback calldata _callback) internal {
AsyncPromise promiseContract = getPromiseContract(_callback);

bytes4 callbackSelector = promiseContract.callbackSelector();
(bool success, bytes memory returnData) = address(this).call(
Expand All @@ -92,6 +119,19 @@ contract AsyncEnabled is SuperchainEnabled {
promiseContract.markResolved();
}

function getPromiseContract(AsyncCallback calldata _callback) internal view returns (AsyncPromise) {
address crossDomainCallbackSender = IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER).crossDomainMessageSender();
uint256 crossDomainCallbackSource = IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER).crossDomainMessageSource();

LocalAsyncProxy remoteProxy = AsyncUtils.calculateLocalAsyncProxyAddress(
address(this),
crossDomainCallbackSender,
crossDomainCallbackSource
);

return remoteProxy.promisesById(_callback.asyncCallId);
}

modifier async() {
// only callable by self via relayAsyncCall
require(msg.sender == address(this));
Expand Down
51 changes: 36 additions & 15 deletions src/AsyncPromise.sol
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {console} from "forge-std/console.sol";

Expand All @@ -16,33 +17,53 @@ contract AsyncPromise {
bytes32 public messageId;
AsyncPromiseState public state = AsyncPromiseState.WAITING_FOR_SET_CALLBACK_SELECTOR;

error OnlyInvokerAllowed();
error PromiseAlreadySetup();

modifier onlyInvoker() {
if (msg.sender != localInvoker) revert OnlyInvokerAllowed();
_;
}

constructor(address _invoker, address _remoteTarget, bytes32 _messageId) {
localInvoker = _invoker;
remoteTarget = _remoteTarget;
messageId = _messageId;
}

function markResolved() external {
require(msg.sender == localInvoker, "Only the invoker can mark this promise's callback resolved");
function markResolved() external onlyInvoker {
_setResolved();
}

function _setResolved() internal {
resolved = true;
state = AsyncPromiseState.RESOLVED;
}

fallback() external {
require(msg.sender == localInvoker, "Only the caller can set this promise's callback");
function _isWaitingForCallback() internal view returns (bool) {
return state == AsyncPromiseState.WAITING_FOR_CALLBACK_EXECUTION;
}

if (state == AsyncPromiseState.WAITING_FOR_CALLBACK_EXECUTION) {
revert("Promise already setup");
}
function _isWaitingForSelector() internal view returns (bool) {
return state == AsyncPromiseState.WAITING_FOR_SET_CALLBACK_SELECTOR;
}

function _setCallbackSelector(bytes calldata data) internal {
callbackSelector = bytes4(data[24:28]);
state = AsyncPromiseState.WAITING_FOR_CALLBACK_EXECUTION;
}

if (state == AsyncPromiseState.WAITING_FOR_SET_CALLBACK_SELECTOR) {
// TODO: is there a way to confirm in the general case this is ".then"?
console.log("got callback selector");
console.logBytes(msg.data);
// 4 bytes for the outer selector, 20 bytes for the address, 4 bytes for the callback selector
// TODO: battle test this against more examples / confirm sufficiently generalized
callbackSelector = bytes4(msg.data[24:28]);
state = AsyncPromiseState.WAITING_FOR_CALLBACK_EXECUTION;
function _handleCallbackSetup(bytes calldata data) internal {
if (_isWaitingForCallback()) {
revert PromiseAlreadySetup();
}

if (_isWaitingForSelector()) {
_setCallbackSelector(data);
}
}

fallback() external onlyInvoker {
_handleCallbackSetup(msg.data);
}
}