Skip to content
Merged
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
2 changes: 1 addition & 1 deletion contracts/evmx/fees/Credit.sol
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ abstract contract Credit is FeesManagerStorage, Initializable, Ownable, AppGatew
address from_,
address to_,
uint256 amount_
) public override returns (bool) {
) public override(ERC20, IFeesManager) returns (bool) {
if (!isCreditSpendable(from_, msg.sender, amount_)) revert InsufficientCreditsAvailable();

if (msg.sender == address(watcher__())) _approve(from_, msg.sender, amount_);
Expand Down
372 changes: 372 additions & 0 deletions contracts/evmx/fees/MessageResolver.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,372 @@
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.21;

import "solady/utils/Initializable.sol";
import "solady/auth/Ownable.sol";
import {ECDSA} from "solady/utils/ECDSA.sol";
import {WATCHER_ROLE} from "../../utils/common/AccessRoles.sol";
import {toBytes32Format} from "../../utils/common/Converters.sol";
import "../../utils/AccessControl.sol";
import "../helpers/AddressResolverUtil.sol";

/**
* @title MessageResolver Storage
* @notice Storage contract for MessageResolver with proper slot management
*/
abstract contract MessageResolverStorage {
// slots [0-49] reserved for gap
uint256[50] _gap_before;

// slot 50
/// @notice Chain slug for EVMx
uint32 public evmxChainSlug;

// Input struct for adding message details
struct MessageDetailsInput {
bytes32 payloadId;
uint32 srcChainSlug;
uint32 dstChainSlug;
bytes32 srcPlug;
bytes32 dstPlug;
uint256 deadline;
address sponsor;
address transmitter;
uint256 feeAmount;
uint256 nonce;
}

// Struct to store message details
struct MessageDetails {
uint32 srcChainSlug;
uint32 dstChainSlug;
bytes32 srcPlug;
bytes32 dstPlug;
uint256 deadline;
address sponsor;
address transmitter;
uint256 feeAmount;
ExecutionStatus status;
}

// Execution status enum
enum ExecutionStatus {
NotAdded, // Message not yet added
Pending, // Message added, awaiting execution
Executed // Payment completed
}

// slot 51
/// @notice Mapping from payloadId to message details
mapping(bytes32 => MessageDetails) public messageDetails;

// slot 52
/// @notice Mapping to track used nonces for watcher signatures
mapping(address => mapping(uint256 => bool)) public usedNonces;

// slots [53-102] reserved for gap
uint256[50] _gap_after;

// slots [103-152] 50 slots reserved for address resolver util
}

/**
* @title MessageResolver
* @notice Contract for resolving payments to transmitters for relaying messages on EVMx
* @dev This contract tracks message details and handles payment settlement after execution
* @dev Uses Credits (ERC20) from FeesManager for payment settlement
* @dev Upgradeable proxy pattern with AddressResolverUtil
*/
contract MessageResolver is MessageResolverStorage, Initializable, AccessControl, AddressResolverUtil {
////////////////////////////////////////////////////////
////////////////////// ERRORS //////////////////////////
////////////////////////////////////////////////////////

/// @notice Thrown when watcher is not authorized
error UnauthorizedWatcher();

/// @notice Thrown when nonce has already been used
error NonceAlreadyUsed();

/// @notice Thrown when message is already added
error MessageAlreadyExists();

/// @notice Thrown when message is not found
error MessageNotFound();

/// @notice Thrown when message is not in pending status
error MessageNotPending();

/// @notice Thrown when payment transfer fails
error PaymentFailed();

/// @notice Thrown when sponsor has insufficient credits
error InsufficientSponsorCredits();

////////////////////////////////////////////////////////
////////////////////// EVENTS //////////////////////////
////////////////////////////////////////////////////////

/// @notice Emitted when message details are added
event MessageDetailsAdded(
bytes32 indexed payloadId,
uint32 srcChainSlug,
uint32 dstChainSlug,
bytes32 srcPlug,
bytes32 dstPlug,
address indexed sponsor,
address indexed transmitter,
uint256 feeAmount,
uint256 deadline
);

/// @notice Emitted when transmitter is paid
event TransmitterPaid(
bytes32 indexed payloadId,
address indexed sponsor,
address indexed transmitter,
uint256 feeAmount
);

/// @notice Emitted when message is marked as executed by watcher
event MessageMarkedExecuted(bytes32 indexed payloadId, address indexed watcher);

////////////////////////////////////////////////////////
////////////////////// CONSTRUCTOR /////////////////////
////////////////////////////////////////////////////////

constructor() {
_disableInitializers(); // disable for implementation
}

/**
* @notice Initializer function to replace constructor for upgradeable contracts
* @param evmxChainSlug_ Chain slug for EVMx
* @param addressResolver_ AddressResolver contract address
* @param owner_ Owner of the contract
*/
function initialize(
uint32 evmxChainSlug_,
address addressResolver_,
address owner_
) public reinitializer(1) {
evmxChainSlug = evmxChainSlug_;
_setAddressResolver(addressResolver_);
_initializeOwner(owner_);
}

////////////////////////////////////////////////////////
////////////////////// FUNCTIONS ///////////////////////
////////////////////////////////////////////////////////

/**
* @notice Add message details for payment resolution
* @dev Called with watcher signature to update details from MessageOutbound event
* @dev Can be routed through watcher for common nonce tracking if needed
* @param input_ Message details input struct
* @param signature_ Watcher signature
*/
function addMessageDetails(
MessageDetailsInput calldata input_,
bytes calldata signature_
) external {
// Verify message doesn't already exist
if (messageDetails[input_.payloadId].status != ExecutionStatus.NotAdded) {
revert MessageAlreadyExists();
}

// Create digest for signature verification
bytes32 digest = keccak256(
abi.encodePacked(
toBytes32Format(address(this)),
evmxChainSlug,
input_.payloadId,
input_.srcChainSlug,
input_.dstChainSlug,
input_.srcPlug,
input_.dstPlug,
input_.deadline,
input_.sponsor,
input_.transmitter,
input_.feeAmount,
input_.nonce
)
);

// Recover signer from signature
address watcher = _recoverSigner(digest, signature_);

// Verify signer has WATCHER_ROLE
if (!_hasRole(WATCHER_ROLE, watcher)) revert UnauthorizedWatcher();

// Check nonce hasn't been used
if (usedNonces[watcher][input_.nonce]) revert NonceAlreadyUsed();
usedNonces[watcher][input_.nonce] = true;

// Store message details
messageDetails[input_.payloadId] = MessageDetails({
srcChainSlug: input_.srcChainSlug,
dstChainSlug: input_.dstChainSlug,
srcPlug: input_.srcPlug,
dstPlug: input_.dstPlug,
deadline: input_.deadline,
sponsor: input_.sponsor,
transmitter: input_.transmitter,
feeAmount: input_.feeAmount,
status: ExecutionStatus.Pending
});

emit MessageDetailsAdded(
input_.payloadId,
input_.srcChainSlug,
input_.dstChainSlug,
input_.srcPlug,
input_.dstPlug,
input_.sponsor,
input_.transmitter,
input_.feeAmount,
input_.deadline
);
}

/**
* @notice Mark message as executed and pay transmitter
* @dev Called by watcher after confirming execution on destination
* @dev Uses Credits from FeesManager for payment
* @param payloadId_ Unique identifier for the payload
* @param signature_ Watcher signature confirming execution
* @param nonce_ Nonce to prevent replay attacks
*/
function markExecuted(
bytes32 payloadId_,
uint256 nonce_,
bytes calldata signature_
) external {
MessageDetails storage details = messageDetails[payloadId_];

// Verify message exists
if (details.status == ExecutionStatus.NotAdded) revert MessageNotFound();

// Verify message is in pending status
if (details.status != ExecutionStatus.Pending) revert MessageNotPending();

// Create digest for signature verification
bytes32 digest = keccak256(
abi.encodePacked(
toBytes32Format(address(this)),
evmxChainSlug,
payloadId_,
nonce_
)
);

// Recover signer from signature
address watcher = _recoverSigner(digest, signature_);

// Verify signer has WATCHER_ROLE
if (!_hasRole(WATCHER_ROLE, watcher)) revert UnauthorizedWatcher();

// Check nonce hasn't been used
if (usedNonces[watcher][nonce_]) revert NonceAlreadyUsed();
usedNonces[watcher][nonce_] = true;

// Check sponsor has sufficient credits (uses AddressResolver to get latest FeesManager)
if (!feesManager__().isCreditSpendable(details.sponsor, address(this), details.feeAmount)) {
revert InsufficientSponsorCredits();
}

// Mark message as executed
details.status = ExecutionStatus.Executed;

// Transfer credits from sponsor to transmitter using FeesManager from AddressResolver
bool success = feesManager__().transferFrom(
details.sponsor,
details.transmitter,
details.feeAmount
);
if (!success) revert PaymentFailed();

emit MessageMarkedExecuted(payloadId_, watcher);
emit TransmitterPaid(
payloadId_,
details.sponsor,
details.transmitter,
details.feeAmount
);
}

////////////////////////////////////////////////////////
////////////////// INTERNAL FUNCTIONS //////////////////
////////////////////////////////////////////////////////

/**
* @notice Recover signer from signature
* @param digest_ The digest that was signed
* @param signature_ The signature
* @return signer The address of the signer
*/
function _recoverSigner(
bytes32 digest_,
bytes memory signature_
) internal view returns (address signer) {
bytes32 ethSignedMessageHash = keccak256(
abi.encodePacked("\x19Ethereum Signed Message:\n32", digest_)
);
signer = ECDSA.recover(ethSignedMessageHash, signature_);
}

////////////////////////////////////////////////////////
////////////////// VIEW FUNCTIONS //////////////////////
////////////////////////////////////////////////////////

/**
* @notice Get message details for a payload
* @param payloadId_ Unique identifier for the payload
* @return Message details struct
*/
function getMessageDetails(
bytes32 payloadId_
) external view returns (MessageDetails memory) {
return messageDetails[payloadId_];
}

/**
* @notice Check if a message is pending
* @param payloadId_ Unique identifier for the payload
* @return True if message is pending execution
*/
function isMessagePending(bytes32 payloadId_) external view returns (bool) {
return messageDetails[payloadId_].status == ExecutionStatus.Pending;
}

/**
* @notice Check if a message is executed
* @param payloadId_ Unique identifier for the payload
* @return True if message is executed and payment completed
*/
function isMessageExecuted(bytes32 payloadId_) external view returns (bool) {
return messageDetails[payloadId_].status == ExecutionStatus.Executed;
}

/**
* @notice Get pending fee amount for a payload
* @param payloadId_ Unique identifier for the payload
* @return Fee amount if pending, 0 otherwise
*/
function getPendingFeeAmount(bytes32 payloadId_) external view returns (uint256) {
MessageDetails memory details = messageDetails[payloadId_];
if (details.status == ExecutionStatus.Pending) {
return details.feeAmount;
}
return 0;
}

/**
* @notice Get execution status for a payload
* @param payloadId_ Unique identifier for the payload
* @return ExecutionStatus enum value
*/
function getExecutionStatus(bytes32 payloadId_) external view returns (ExecutionStatus) {
return messageDetails[payloadId_].status;
}
}

6 changes: 6 additions & 0 deletions contracts/evmx/interfaces/IFeesManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,10 @@ interface IFeesManager {
function isApproved(address appGateway_, address user_) external view returns (bool);

function setMaxFees(uint256 fees_) external;

function transferFrom(
address from_,
address to_,
uint256 amount_
) external returns (bool);
}
Loading