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
26 changes: 11 additions & 15 deletions packages/evm/contracts/Intents.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ pragma solidity ^0.8.20;

/**
* @dev Enum representing the operation type.
* - Swap: Swap tokens between chains or tokens.
* - Swap: Swap tokens in the same chain.
* - Transfer: Transfer tokens to one or more recipients.
* - Call: Execute arbitrary contract calls.
* - CrossChainSwap: Swap tokens between chains.
*/
enum OpType {
Swap,
Transfer,
Call
Call,
CrossChainSwap
}

/**
Expand Down Expand Up @@ -187,7 +189,7 @@ struct SwapProposal {
library IntentsHelpers {
bytes32 internal constant INTENT_TYPE_HASH =
keccak256(
'Intent(address feePayer,address settler,bytes32 nonce,uint256 deadline,MaxFee[] maxFees,bytes triggerSig,uint256 minValidations,Operation[] operations)MaxFee(address token,uint256 amount)Operation(uint8 opType,address user,bytes data,OperationEvent[] events,bytes32 intentNonce,uint256 index)OperationEvent(bytes32 topic,bytes data)'
'Intent(address feePayer,address settler,bytes32 nonce,uint256 deadline,MaxFee[] maxFees,bytes triggerSig,uint256 minValidations,Operation[] operations)MaxFee(address token,uint256 amount)Operation(uint8 opType,address user,bytes data,OperationEvent[] events)OperationEvent(bytes32 topic,bytes data)'
);

bytes32 internal constant PROPOSAL_TYPE_HASH =
Expand All @@ -198,9 +200,7 @@ library IntentsHelpers {
bytes32 internal constant MAX_FEE_TYPE_HASH = keccak256('MaxFee(address token,uint256 amount)');

bytes32 internal constant OPERATION_TYPE_HASH =
keccak256(
'Operation(uint8 opType,address user,bytes data,OperationEvent[] events,bytes32 intentNonce,uint256 index)'
);
keccak256('Operation(uint8 opType,address user,bytes data,OperationEvent[] events)');

bytes32 internal constant OPERATION_EVENT_TYPE_HASH = keccak256('OperationEvent(bytes32 topic,bytes data)');

Expand All @@ -216,7 +216,7 @@ library IntentsHelpers {
hash(intent.maxFees),
intent.triggerSig,
intent.minValidations,
hash(intent.operations, intent.nonce)
hash(intent.operations)
)
);
}
Expand All @@ -243,25 +243,21 @@ library IntentsHelpers {
return keccak256(abi.encodePacked(hashes));
}

function hash(Operation[] memory operations, bytes32 intentNonce) internal pure returns (bytes32) {
function hash(Operation[] memory operations) internal pure returns (bytes32) {
bytes32[] memory hashes = new bytes32[](operations.length);
for (uint256 i = 0; i < operations.length; i++) {
hashes[i] = hash(operations[i], intentNonce, i);
}
for (uint256 i = 0; i < operations.length; i++) hashes[i] = hash(operations[i]);
return keccak256(abi.encodePacked(hashes));
}

function hash(Operation memory operation, bytes32 intentNonce, uint256 index) internal pure returns (bytes32) {
function hash(Operation memory operation) internal pure returns (bytes32) {
return
keccak256(
abi.encode(
OPERATION_TYPE_HASH,
operation.opType,
operation.user,
keccak256(operation.data),
hash(operation.events),
intentNonce,
index
hash(operation.events)
)
);
}
Expand Down
66 changes: 47 additions & 19 deletions packages/evm/contracts/Settler.sol
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ contract Settler is ISettler, Ownable, ReentrancyGuard, EIP712 {
using SafeERC20 for IERC20;
using IntentsHelpers for Intent;
using IntentsHelpers for Proposal;
using IntentsHelpers for Operation;
using IntentsHelpers for Validation;
using SmartAccountsHandlerHelpers for address;

Expand Down Expand Up @@ -202,8 +201,9 @@ contract Settler is ISettler, Ownable, ReentrancyGuard, EIP712 {

for (uint256 i = 0; i < intent.operations.length; i++) {
uint8 opType = intent.operations[i].opType;
if (opType == uint8(OpType.Swap)) _executeSwap(intent, proposal, i);
else if (opType == uint8(OpType.Transfer)) _executeTransfer(intent, proposal, i);
if (opType == uint8(OpType.Swap) || opType == uint8(OpType.CrossChainSwap)) {
_executeSwap(intent, proposal, i);
} else if (opType == uint8(OpType.Transfer)) _executeTransfer(intent, proposal, i);
else if (opType == uint8(OpType.Call)) _executeCall(intent, proposal, i);
else revert SettlerUnknownOperationType(uint8(opType));
}
Expand All @@ -221,7 +221,10 @@ contract Settler is ISettler, Ownable, ReentrancyGuard, EIP712 {
Operation memory operation = intent.operations[index];
SwapOperation memory swapOperation = abi.decode(operation.data, (SwapOperation));
SwapProposal memory swapProposal = abi.decode(proposal.datas[index], (SwapProposal));
_validateSwapOperation(swapOperation, swapProposal);
if (operation.opType == uint8(OpType.CrossChainSwap)) {
if (intent.operations.length > 1) revert SettlerCrossChainSwapMustBeOnlyOperation();
_validateCrossChainSwapOperation(swapOperation, swapProposal);
} else _validateSingleChainSwapOperation(swapOperation, swapProposal);

bool isSmartAccount = smartAccountsHandler.isSmartAccount(operation.user);
if (swapOperation.sourceChain == block.chainid) {
Expand All @@ -232,8 +235,7 @@ contract Settler is ISettler, Ownable, ReentrancyGuard, EIP712 {
}

uint256[] memory preBalancesOut = _getTokensOutBalance(swapOperation);
bytes32 operationHash = operation.hash(intent.nonce, index);
IExecutor(swapProposal.executor).execute(operation, operationHash, proposal.datas[index]);
IExecutor(swapProposal.executor).execute(intent, proposal, index);

if (swapOperation.destinationChain == block.chainid) {
uint256[] memory outputs = new uint256[](swapOperation.tokensOut.length);
Expand Down Expand Up @@ -299,7 +301,6 @@ contract Settler is ISettler, Ownable, ReentrancyGuard, EIP712 {
* @dev Validates an intent and its corresponding proposal
The off-chain validators are assuring that:
- The trigger signer has authorization over the intent.feePayer and each operations[i].user
- If there is a cross-chain swap operation, it is the last one
* @param intent Intent to be fulfilled
* @param proposal Proposal to be executed
* @param signature Proposal signature
Expand Down Expand Up @@ -370,9 +371,6 @@ contract Settler is ISettler, Ownable, ReentrancyGuard, EIP712 {
* @param proposal Proposal to be executed
*/
function _validateSwapOperation(SwapOperation memory operation, SwapProposal memory proposal) internal view {
bool isChainInvalid = operation.sourceChain != block.chainid && operation.destinationChain != block.chainid;
if (isChainInvalid) revert SettlerInvalidChain(block.chainid);

if (proposal.amountsOut.length != operation.tokensOut.length) revert SettlerInvalidProposedAmounts();

for (uint256 i = 0; i < operation.tokensOut.length; i++) {
Expand All @@ -384,11 +382,20 @@ contract Settler is ISettler, Ownable, ReentrancyGuard, EIP712 {
uint256 proposedAmount = proposal.amountsOut[i];
if (proposedAmount < minAmount) revert SettlerProposedAmountLtMinAmount(i, proposedAmount, minAmount);
}
}

if (operation.sourceChain != operation.destinationChain) {
bool isExecutorInvalid = !IController(controller).isExecutorAllowed(proposal.executor);
if (isExecutorInvalid) revert SettlerExecutorNotAllowed(proposal.executor);
}
/**
* @dev Validates a single-chain swap operation and its corresponding proposal
* @param operation Swap operation to be fulfilled
* @param proposal Proposal to be executed
*/
function _validateSingleChainSwapOperation(SwapOperation memory operation, SwapProposal memory proposal)
internal
view
{
if (operation.sourceChain != operation.destinationChain) revert SettlerOperationChainsMismatch();
if (operation.sourceChain != block.chainid) revert SettlerInvalidChain(block.chainid);
_validateSwapOperation(operation, proposal);
}

/**
Expand Down Expand Up @@ -420,6 +427,23 @@ contract Settler is ISettler, Ownable, ReentrancyGuard, EIP712 {
if (!smartAccountsHandler.isSmartAccount(user)) revert SettlerUserNotSmartAccount(user);
}

/**
* @dev Validates a cross-chain swap operation and its corresponding proposal
* @param operation Swap operation to be fulfilled
* @param proposal Proposal to be executed
*/
function _validateCrossChainSwapOperation(SwapOperation memory operation, SwapProposal memory proposal)
internal
view
{
if (operation.sourceChain == operation.destinationChain) revert SettlerOperationChainsMismatch();
bool isChainInvalid = operation.sourceChain != block.chainid && operation.destinationChain != block.chainid;
if (isChainInvalid) revert SettlerInvalidChain(block.chainid);
_validateSwapOperation(operation, proposal);
bool isExecutorInvalid = !IController(controller).isExecutorAllowed(proposal.executor);
if (isExecutorInvalid) revert SettlerExecutorNotAllowed(proposal.executor);
}

/**
* @dev Tells the contract balance for each token out of a swap operation
* @param operation Swap operation containing the list of tokens out
Expand All @@ -439,11 +463,10 @@ contract Settler is ISettler, Ownable, ReentrancyGuard, EIP712 {
* @param intent Intent to be fulfilled
*/
function _shouldValidateDeadlines(Intent memory intent) internal view returns (bool) {
// Validators ensure off-chain that a cross-chain operation can only be the last operation
Operation memory finalOperation = intent.operations[intent.operations.length - 1];
if (finalOperation.opType != uint8(OpType.Swap)) return true;
SwapOperation memory swapIntent = abi.decode(finalOperation.data, (SwapOperation));
if (swapIntent.sourceChain == swapIntent.destinationChain) return true;
// Cross-chain operation can only be the first (and only) operation
Operation memory operation = intent.operations[0];
if (operation.opType != uint8(OpType.CrossChainSwap)) return true;
SwapOperation memory swapIntent = abi.decode(operation.data, (SwapOperation));
return swapIntent.sourceChain == block.chainid;
}

Expand Down Expand Up @@ -484,6 +507,11 @@ contract Settler is ISettler, Ownable, ReentrancyGuard, EIP712 {
* @param proposal Proposal to be executed
*/
function _payFees(Intent memory intent, Proposal memory proposal) internal {
// A CrossChainSwap only pays fees on destination chain and must be the only operation
if (intent.operations[0].opType == uint8(OpType.CrossChainSwap)) {
SwapOperation memory swapOperation = abi.decode(intent.operations[0].data, (SwapOperation));
if (swapOperation.sourceChain == block.chainid) return;
}
address from = intent.feePayer;
address to = _msgSender();
bool isSmartAccount = smartAccountsHandler.isSmartAccount(from);
Expand Down
8 changes: 4 additions & 4 deletions packages/evm/contracts/interfaces/IExecutor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import '../Intents.sol';
interface IExecutor {
/**
* @dev Executes an operation proposal
* @param operation Operation to be executed
* @param operationHash unique hash of the operation
* @param proposalData data of the proposal to be executed to fulfill the operation
* @param intent Intent that contains swap operation to be executed
* @param proposal Proposal with swap data to be executed
* @param index Position where the swap proposal data and operation are located
*/
function execute(Operation memory operation, bytes32 operationHash, bytes memory proposalData) external;
function execute(Intent memory intent, Proposal memory proposal, uint256 index) external;
}
9 changes: 7 additions & 2 deletions packages/evm/contracts/interfaces/ISettler.sol
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,14 @@ interface ISettler {
error SettlerTooManySafeguards(uint256 lengthRequested);

/**
* @dev The chain of an operation does not match the rest of the intent's operations
* @dev The chains of a swap operation do not match the swap type (single or cross chain)
*/
error SettlerOperationChainMismatch(uint256 expected, uint256 actual);
error SettlerOperationChainsMismatch();

/**
* @dev A CrossChainSwap operation must be the only operation in the intent
*/
error SettlerCrossChainSwapMustBeOnlyOperation();

/**
* @dev The new smart accounts handler is zero
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import '../../interfaces/IExecutor.sol';
contract EmptyExecutorMock is IExecutor {
event Executed();

function execute(Operation memory, bytes32, bytes memory) external override {
function execute(Intent memory, Proposal memory, uint256) external override {
emit Executed();
}
}
12 changes: 8 additions & 4 deletions packages/evm/contracts/test/executors/MintExecutorMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@ import '../../interfaces/IExecutor.sol';
contract MintExecutorMock is IExecutor {
event Minted();

function execute(Operation memory operation, bytes32, bytes memory proposalData) external override {
require(operation.opType == uint8(OpType.Swap), 'Invalid operation type');

SwapProposal memory swapProposal = abi.decode(proposalData, (SwapProposal));
function execute(Intent memory intent, Proposal memory proposal, uint256 index) external override {
Operation memory operation = intent.operations[index];
require(
operation.opType == uint8(OpType.Swap) || operation.opType == uint8(OpType.CrossChainSwap),
'Invalid operation type'
);

SwapProposal memory swapProposal = abi.decode(proposal.datas[index], (SwapProposal));
(address[] memory tokens, uint256[] memory amounts) = abi.decode(swapProposal.data, (address[], uint256[]));

require(tokens.length == amounts.length, 'Invalid inputs');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ contract ReentrantExecutorMock is IExecutor {
settler = _settler;
}

function execute(Operation memory, bytes32, bytes memory) external override {
Intent memory intent;
Proposal memory proposal;
function execute(Intent memory intent, Proposal memory proposal, uint256) external override {
ISettler(settler).execute(intent, proposal, new bytes(0));
}
}
12 changes: 8 additions & 4 deletions packages/evm/contracts/test/executors/TransferExecutorMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,14 @@ contract TransferExecutorMock is IExecutor {
// solhint-disable-previous-line no-empty-blocks
}

function execute(Operation memory operation, bytes32, bytes memory proposalData) external override {
require(operation.opType == uint8(OpType.Swap), 'Invalid operation type');

SwapProposal memory swapProposal = abi.decode(proposalData, (SwapProposal));
function execute(Intent memory intent, Proposal memory proposal, uint256 index) external override {
Operation memory operation = intent.operations[index];
require(
operation.opType == uint8(OpType.Swap) || operation.opType == uint8(OpType.CrossChainSwap),
'Invalid operation type'
);

SwapProposal memory swapProposal = abi.decode(proposal.datas[index], (SwapProposal));
(address[] memory tokens, uint256[] memory amounts) = abi.decode(swapProposal.data, (address[], uint256[]));

require(tokens.length == amounts.length, 'Invalid inputs');
Expand Down
Loading
Loading