Skip to content
This repository has been archived by the owner on Nov 29, 2023. It is now read-only.

Commit

Permalink
Merge pull request #1 from magmo/cross-chain-exec
Browse files Browse the repository at this point in the history
Cross chain execution and payments via UserOps
  • Loading branch information
lalexgap authored Nov 29, 2023
2 parents 7587425 + ded0c82 commit 99c7c3c
Show file tree
Hide file tree
Showing 6 changed files with 497 additions and 48 deletions.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.sol linguist-language=Solidity
130 changes: 120 additions & 10 deletions contracts/SCBridgeWallet.sol
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
pragma solidity 0.8.19;

// SPDX-License-Identifier: MIT

import {IAccount} from "contracts/interfaces/IAccount.sol";
import {UserOperation} from "contracts/interfaces/UserOperation.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {HTLC, State, hashState, checkSignatures, Participant} from "./state.sol";
import {UserOperationLib} from "contracts/core/UserOperationLib.sol";
import {ExecuteChainInfo, PaymentChainInfo} from "./state.sol";

enum WalletStatus {
OPEN,
CHALLENGE_RAISED,
FINALIZED
}

enum NonceKey {
SHARED,
OWNER
}

uint constant CHALLENGE_WAIT = 1 days;

contract SCBridgeWallet is IAccount {
Expand Down Expand Up @@ -100,7 +107,10 @@ contract SCBridgeWallet is IAccount {
bytes calldata intermediarySignature
) external {
checkSignatures(state, ownerSignature, intermediarySignature);
_challenge(state);
}

function _challenge(State calldata state) internal {
WalletStatus status = getStatus();

require(status != WalletStatus.FINALIZED, "Wallet already finalized");
Expand All @@ -125,7 +135,26 @@ contract SCBridgeWallet is IAccount {
challengeExpiry = largestTimeLock + CHALLENGE_WAIT;
}

function execute(address dest, uint256 value, bytes calldata func) external {
/// crossChain is a special function that handles cross chain execution and payment
function crossChain(
ExecuteChainInfo[] calldata e,
PaymentChainInfo[] calldata p
) public {
// Only the entrypoint should trigger this by excecuting a UserOp
require(msg.sender == entrypoint, "account: not EntryPoint");
for (uint i = 0; i < e.length; i++) {
if (e[i].chainId == block.chainid) {
execute(e[i].dest, e[i].value, e[i].callData);
}
}
for (uint i = 0; i < p.length; i++) {
if (p[i].chainId == block.chainid) {
_challenge(p[i].paymentState);
}
}
}

function execute(address dest, uint256 value, bytes calldata func) public {
if (getStatus() == WalletStatus.FINALIZED && activeHTLCs.length == 0) {
// If the wallet has finalized and all the funds have been reclaimed then the owner can do whatever they want with the remaining funds
// The owner can call this function directly or the entrypoint can call it on their behalf
Expand Down Expand Up @@ -160,24 +189,34 @@ contract SCBridgeWallet is IAccount {
) external view returns (uint256 validationData) {
bytes memory ownerSig = userOp.signature[0:65];
// The owner of the wallet must always approve of any user operation to execute on it's behalf
require(
validateSignature(userOpHash, ownerSig, owner) ==
SIG_VALIDATION_SUCCEEDED,
"owner must sign"
);
if (
validateSignature(userOpHash, ownerSig, owner) != SIG_VALIDATION_SUCCEEDED
) {
return SIG_VALIDATION_FAILED;
}

// If the wallet is finalized then the owner can do whatever they want with the remaining funds
if (getStatus() == WalletStatus.FINALIZED) {
return SIG_VALIDATION_SUCCEEDED;
}

bytes4 functionSelector = bytes4(userOp.callData[0:4]);
NonceKey key = NonceKey(userOp.nonce >> 64);

// If the function is crossChain, we validate using the chainids and entrypoints from the calldata
if (
functionSelector == this.crossChain.selector && key == NonceKey.SHARED
) {
validateCrossChain(userOp);
}

// If the function is permitted, it can be called at any time
// (including when the wallet is in CHALLENGE_RAISED) with no futher checks.
bytes4 functionSelector = bytes4(userOp.callData[0:4]);
if (permitted(functionSelector)) return SIG_VALIDATION_SUCCEEDED;
if (permitted(functionSelector) && key == NonceKey.OWNER)
return SIG_VALIDATION_SUCCEEDED;

// If the wallet is open, we need to apply extra conditions:
if (getStatus() == WalletStatus.OPEN) {
if (getStatus() == WalletStatus.OPEN && key == NonceKey.SHARED) {
bytes memory intermediarySig = userOp.signature[65:130];
return validateSignature(userOpHash, intermediarySig, intermediary);
}
Expand All @@ -194,6 +233,67 @@ contract SCBridgeWallet is IAccount {
uint256 internal constant SIG_VALIDATION_SUCCEEDED = 0;
uint256 internal constant SIG_VALIDATION_FAILED = 1;

/// This validates the crossChain UserOp
/// It ensures that it signed by the owner and intermediary on every chain
/// It also ensures that the UserOp targets the current chain and entrypoint
function validateCrossChain(UserOperation calldata userOp) private view {
(ExecuteChainInfo[] memory e, PaymentChainInfo[] memory p) = abi.decode(
userOp.callData[4:],
(ExecuteChainInfo[], PaymentChainInfo[])
);

bool foundExecute = false;
bool foundPayment = false;

// We expect every owner and intermediary on every chain to have signed the userOpHash
// For each chain we expect a signature from the owner and intermediary
require(
userOp.signature.length == (e.length + p.length) * 65 * 2,
"Invalid signature length"
);
// We expect every participant to sign the UserOpHash generated against the first entrypoint and chainId
bytes32 userOpHash;
require(e.length + p.length > 0, "Must target at least one chain");
if (e.length > 0) {
userOpHash = generateUserOpHash(userOp, e[0].entrypoint, e[0].chainId);
} else {
userOpHash = generateUserOpHash(userOp, p[0].entrypoint, p[0].chainId);
}

for (uint i = 0; i < e.length; i++) {
if (e[i].chainId == block.chainid && e[i].entrypoint == entrypoint) {
foundExecute = true;
}

uint offset = i * 65;
bytes memory ownerSig = userOp.signature[offset:offset + 65];
bytes memory intermediarySig = userOp.signature[offset + 65:offset + 130];
validateSignature(userOpHash, ownerSig, e[i].owner);
validateSignature(userOpHash, intermediarySig, e[i].intermediary);
}

for (uint i = 0; i < p.length; i++) {
if (p[i].chainId == block.chainid && p[i].entrypoint == entrypoint) {
foundPayment = true;
}

uint offset = (e.length + i) * 65;
bytes memory ownerSig = userOp.signature[offset:offset + 65];
bytes memory intermediarySig = userOp.signature[offset + 65:offset + 130];
validateSignature(userOpHash, ownerSig, p[i].paymentState.owner);
validateSignature(
userOpHash,
intermediarySig,
p[i].paymentState.intermediary
);
}

require(
foundExecute || foundPayment,
"Must target execution or payment chain"
);
}

function validateSignature(
bytes32 userOpHash,
bytes memory signature,
Expand All @@ -215,3 +315,13 @@ contract SCBridgeWallet is IAccount {
return true;
}
}
using UserOperationLib for UserOperation;

/// @dev Based on the entrypoint implementation
function generateUserOpHash(
UserOperation calldata userOp,
address entrypoint,
uint chainId
) pure returns (bytes32) {
return keccak256(abi.encode(userOp.hash(), entrypoint, chainId));
}
73 changes: 45 additions & 28 deletions contracts/state.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,47 +3,64 @@ pragma solidity 0.8.19;
// SPDX-License-Identifier: MIT
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
struct State {
address payable owner;
address payable intermediary;
uint turnNum;
uint intermediaryBalance;
HTLC[] htlcs;
address payable owner;
address payable intermediary;
uint turnNum;
uint intermediaryBalance;
HTLC[] htlcs;
}

enum Participant {
OWNER,
INTERMEDIARY
OWNER,
INTERMEDIARY
}

struct HTLC {
Participant to;
uint amount;
bytes32 hashLock;
uint timelock;
Participant to;
uint amount;
bytes32 hashLock;
uint timelock;
}

function hashState(State memory state) pure returns (bytes32) {
return keccak256(abi.encode(state));
return keccak256(abi.encode(state));
}

using ECDSA for bytes32;

function checkSignatures(
State calldata state,
bytes calldata ownerSignature,
bytes calldata intermediarySignature
State calldata state,
bytes calldata ownerSignature,
bytes calldata intermediarySignature
) pure {
bytes32 stateHash = hashState(state);
if (
state.owner !=
stateHash.toEthSignedMessageHash().recover(ownerSignature)
) {
revert("Invalid owner signature");
}
if (
state.intermediary !=
stateHash.toEthSignedMessageHash().recover(intermediarySignature)
) {
revert("Invalid intermediary signature");
}
bytes32 stateHash = hashState(state);
if (
state.owner != stateHash.toEthSignedMessageHash().recover(ownerSignature)
) {
revert("Invalid owner signature");
}
if (
state.intermediary !=
stateHash.toEthSignedMessageHash().recover(intermediarySignature)
) {
revert("Invalid intermediary signature");
}
}

/// Contains information to execute a function call
struct ExecuteChainInfo {
uint chainId;
address entrypoint;
address dest;
uint value;
bytes callData;
address owner;
address intermediary;
}

/// Contains information to execute a payment
struct PaymentChainInfo {
uint chainId;
address entrypoint;
State paymentState;
}
Binary file added docs/2-hop-rpc.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 99c7c3c

Please sign in to comment.