-
Notifications
You must be signed in to change notification settings - Fork 0
EVM: Add support for dynamic call intents #52
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
Merged
alavarello
merged 22 commits into
evm/refactor-intents-with-operations
from
evm/dynamic_call_intents
Apr 30, 2026
Merged
Changes from all commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
6a148fe
evm: implement dynamic call encoder library
facuspagnuolo 1f0a490
evm: add bytes helpers tests
facuspagnuolo d3e4d30
evm: add new dynamic call intent type
facuspagnuolo 3922377
Merge branch 'evm/refactor-intents-with-operations' into evm/dynamic_…
lgalende 4a73699
chore: rename file and struct
lgalende da11533
evm: unwrap delegatecall return in SmartAccountsHandlerHelpers
lgalende d8de41a
evm: add array support to bytes helpers
lgalende 0664c83
evm: add dynamic call encoder interface
lgalende 862c2d8
evm: add dynamic call to settler
lgalende ed04df6
evm: add dynamic call operations validator
lgalende d09a02c
evm: allow dynamic calls to reference any operation output
lgalende 8abb986
evm: avoid slicing outputs in dynamic call execution
lgalende 826cdf7
evm: add dynamic call encoder setter
lgalende 4d82582
chore: remove lint warnings
lgalende 0a045c5
evm: remove staticcall dynamic arg kind
lgalende 19f878a
evm: rename DynamicCallEncoder error
lgalende aaa07af
evm: optimize SmartAccountsHandlerHelpers call
lgalende f87311b
test: add chained references and mixed kinds case
lgalende aef09b0
evm: explicit dynamic attribute
lgalende ed275cb
evm: add readWord, use mcopy, and remove lastWordIsZero
lgalende d74796f
chore: upgrade sdk to v0.0.2-rc.1
lgalende 57ea8b3
SDK: Bump version
alavarello File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,7 +23,9 @@ import '@openzeppelin/contracts/utils/cryptography/EIP712.sol'; | |
| import '@openzeppelin/contracts/utils/introspection/ERC165Checker.sol'; | ||
|
|
||
| import './Intents.sol'; | ||
| import './dynamic-calls/DynamicCallEncoder.sol'; | ||
| import './interfaces/IController.sol'; | ||
| import './interfaces/IDynamicCallEncoder.sol'; | ||
| import './interfaces/IOperationsValidator.sol'; | ||
| import './interfaces/IExecutor.sol'; | ||
| import './interfaces/ISettler.sol'; | ||
|
|
@@ -53,6 +55,9 @@ contract Settler is ISettler, Ownable, ReentrancyGuard, EIP712 { | |
| // Operations validator reference | ||
| address public override operationsValidator; | ||
|
|
||
| // Dynamic call encoder reference | ||
| address public dynamicCallEncoder; | ||
|
|
||
| // List of block numbers at which an intent was executed | ||
| mapping (bytes32 => uint256) public override getIntentBlock; | ||
|
|
||
|
|
@@ -76,6 +81,7 @@ contract Settler is ISettler, Ownable, ReentrancyGuard, EIP712 { | |
| constructor(address _controller, address _owner) Ownable(_owner) EIP712('Mimic Protocol Settler', '1') { | ||
| controller = _controller; | ||
| smartAccountsHandler = address(new SmartAccountsHandler()); | ||
| dynamicCallEncoder = address(new DynamicCallEncoder()); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -145,6 +151,14 @@ contract Settler is ISettler, Ownable, ReentrancyGuard, EIP712 { | |
| _setOperationsValidator(newOperationsValidator); | ||
| } | ||
|
|
||
| /** | ||
| * @dev Sets a new dynamic call encoder address | ||
| * @param newDynamicCallEncoder New dynamic call encoder to be set | ||
| */ | ||
| function setDynamicCallEncoder(address newDynamicCallEncoder) external override onlyOwner { | ||
| _setDynamicCallEncoder(newDynamicCallEncoder); | ||
| } | ||
|
|
||
| /** | ||
| * @dev Sets a safeguard for a user | ||
| * @param safeguard Safeguard to be set | ||
|
|
@@ -199,25 +213,46 @@ contract Settler is ISettler, Ownable, ReentrancyGuard, EIP712 { | |
| _validateIntent(intent, proposal, signature, simulated); | ||
| getIntentBlock[intent.hash()] = block.number; | ||
|
|
||
| bytes[][] memory outputs = new bytes[][](intent.operations.length); | ||
| for (uint256 i = 0; i < intent.operations.length; i++) { | ||
| uint8 opType = intent.operations[i].opType; | ||
| 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)); | ||
| outputs[i] = _executeOperation(intent, proposal, i, outputs); | ||
| } | ||
|
|
||
| _payFees(intent, proposal); | ||
| emit ProposalExecuted(proposal.hash(intent, _msgSender())); | ||
| } | ||
|
|
||
| /** | ||
| * @dev Executes proposal to fulfill an operation | ||
| * @param intent Intent being fulfilled | ||
| * @param proposal Proposal being executed | ||
| * @param index Position where the operation and its corresponding proposal data are located | ||
| * @param outputs List of operations outputs | ||
| */ | ||
| function _executeOperation(Intent memory intent, Proposal memory proposal, uint256 index, bytes[][] memory outputs) | ||
| internal | ||
| returns (bytes[] memory) | ||
| { | ||
| uint8 opType = intent.operations[index].opType; | ||
| if (opType == uint8(OpType.Swap) || opType == uint8(OpType.CrossChainSwap)) { | ||
| return _executeSwap(intent, proposal, index); | ||
| } | ||
| if (opType == uint8(OpType.Transfer)) return _executeTransfer(intent, proposal, index); | ||
| if (opType == uint8(OpType.Call)) return _executeCall(intent, proposal, index); | ||
| if (opType == uint8(OpType.DynamicCall)) return _executeDynamicCall(intent, proposal, index, outputs); | ||
| revert SettlerUnknownOperationType(opType); | ||
| } | ||
|
|
||
| /** | ||
| * @dev Validates and executes a proposal to fulfill a swap operation | ||
| * @param intent Intent that contains swap operation to be fulfilled | ||
| * @param proposal Proposal with swap data to be executed | ||
| * @param index Position where the swap proposal data and operation are located | ||
| */ | ||
| function _executeSwap(Intent memory intent, Proposal memory proposal, uint256 index) internal { | ||
| function _executeSwap(Intent memory intent, Proposal memory proposal, uint256 index) | ||
| internal | ||
| returns (bytes[] memory outputs) | ||
| { | ||
| Operation memory operation = intent.operations[index]; | ||
| SwapOperation memory swapOperation = abi.decode(operation.data, (SwapOperation)); | ||
| SwapProposal memory swapProposal = abi.decode(proposal.datas[index], (SwapProposal)); | ||
|
|
@@ -237,22 +272,24 @@ contract Settler is ISettler, Ownable, ReentrancyGuard, EIP712 { | |
| uint256[] memory preBalancesOut = _getTokensOutBalance(swapOperation); | ||
| IExecutor(swapProposal.executor).execute(intent, proposal, index); | ||
|
|
||
| outputs = new bytes[](swapOperation.tokensOut.length); | ||
| if (swapOperation.destinationChain == block.chainid) { | ||
| uint256[] memory outputs = new uint256[](swapOperation.tokensOut.length); | ||
| uint256[] memory amounts = new uint256[](swapOperation.tokensOut.length); | ||
| for (uint256 i = 0; i < swapOperation.tokensOut.length; i++) { | ||
| TokenOut memory tokenOut = swapOperation.tokensOut[i]; | ||
| uint256 postBalanceOut = ERC20Helpers.balanceOf(tokenOut.token, address(this)); | ||
| uint256 preBalanceOut = preBalancesOut[i]; | ||
| if (postBalanceOut < preBalanceOut) revert SettlerPostBalanceOutLtPre(i, postBalanceOut, preBalanceOut); | ||
|
|
||
| outputs[i] = postBalanceOut - preBalanceOut; | ||
| amounts[i] = postBalanceOut - preBalanceOut; | ||
| uint256 proposedAmount = swapProposal.amountsOut[i]; | ||
| if (outputs[i] < proposedAmount) revert SettlerAmountOutLtProposed(i, outputs[i], proposedAmount); | ||
| if (amounts[i] < proposedAmount) revert SettlerAmountOutLtProposed(i, amounts[i], proposedAmount); | ||
|
|
||
| ERC20Helpers.transfer(tokenOut.token, tokenOut.recipient, outputs[i]); | ||
| ERC20Helpers.transfer(tokenOut.token, tokenOut.recipient, amounts[i]); | ||
| outputs[i] = abi.encode(amounts[i]); | ||
| } | ||
|
|
||
| _emitOperationEvents(operation, proposal, intent.hash(), index, abi.encode(outputs)); | ||
| _emitOperationEvents(operation, proposal, intent.hash(), index, abi.encode(amounts)); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -262,7 +299,10 @@ contract Settler is ISettler, Ownable, ReentrancyGuard, EIP712 { | |
| * @param proposal Transfer proposal to be executed | ||
| * @param index Position where the trasnfer proposal data and operation are located | ||
| */ | ||
| function _executeTransfer(Intent memory intent, Proposal memory proposal, uint256 index) internal { | ||
| function _executeTransfer(Intent memory intent, Proposal memory proposal, uint256 index) | ||
| internal | ||
| returns (bytes[] memory outputs) | ||
| { | ||
| Operation memory operation = intent.operations[index]; | ||
| TransferOperation memory transferOperation = abi.decode(operation.data, (TransferOperation)); | ||
| _validateTransferOperation(transferOperation, proposal.datas[index]); | ||
|
|
@@ -273,6 +313,7 @@ contract Settler is ISettler, Ownable, ReentrancyGuard, EIP712 { | |
| _transferFrom(transfer.token, operation.user, transfer.recipient, transfer.amount, isSmartAccount); | ||
| } | ||
|
|
||
| outputs = new bytes[](0); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could include the balance diff instead (as we do in swaps). If we decide to do so, I'd do it in a separate PR. |
||
| _emitOperationEvents(operation, proposal, intent.hash(), index, new bytes(0)); | ||
| } | ||
|
|
||
|
|
@@ -282,12 +323,15 @@ contract Settler is ISettler, Ownable, ReentrancyGuard, EIP712 { | |
| * @param proposal Call proposal to be executed | ||
| * @param index Position where the call proposal data and operation are located | ||
| */ | ||
| function _executeCall(Intent memory intent, Proposal memory proposal, uint256 index) internal { | ||
| function _executeCall(Intent memory intent, Proposal memory proposal, uint256 index) | ||
| internal | ||
| returns (bytes[] memory outputs) | ||
| { | ||
| Operation memory operation = intent.operations[index]; | ||
| CallOperation memory callOperation = abi.decode(operation.data, (CallOperation)); | ||
| _validateCallOperation(callOperation, proposal.datas[index], operation.user); | ||
|
|
||
| bytes[] memory outputs = new bytes[](callOperation.calls.length); | ||
| outputs = new bytes[](callOperation.calls.length); | ||
| for (uint256 i = 0; i < callOperation.calls.length; i++) { | ||
| CallData memory call = callOperation.calls[i]; | ||
| // solhint-disable-next-line avoid-low-level-calls | ||
|
|
@@ -297,6 +341,34 @@ contract Settler is ISettler, Ownable, ReentrancyGuard, EIP712 { | |
| _emitOperationEvents(operation, proposal, intent.hash(), index, abi.encode(outputs)); | ||
| } | ||
|
|
||
| /** | ||
| * @dev Validates and executes a proposal to fulfill a dynamic call operation | ||
| * @param intent Intent that contains dynamic call operation to be fulfilled | ||
| * @param proposal Dynamic call proposal to be executed | ||
| * @param index Position where the dynamic call proposal data and operation are located | ||
| * @param variables List of operations outputs | ||
| */ | ||
| function _executeDynamicCall( | ||
| Intent memory intent, | ||
| Proposal memory proposal, | ||
| uint256 index, | ||
| bytes[][] memory variables | ||
| ) internal returns (bytes[] memory outputs) { | ||
| Operation memory operation = intent.operations[index]; | ||
| DynamicCallOperation memory dynamicCallOperation = abi.decode(operation.data, (DynamicCallOperation)); | ||
| _validateDynamicCallOperation(dynamicCallOperation, proposal.datas[index], operation.user); | ||
|
|
||
| outputs = new bytes[](dynamicCallOperation.calls.length); | ||
| for (uint256 i = 0; i < dynamicCallOperation.calls.length; i++) { | ||
| DynamicCall memory dynamicCall = abi.decode(dynamicCallOperation.calls[i], (DynamicCall)); | ||
| bytes memory data = IDynamicCallEncoder(dynamicCallEncoder).encode(dynamicCall, variables, index); | ||
| // solhint-disable-next-line avoid-low-level-calls | ||
| outputs[i] = smartAccountsHandler.call(operation.user, dynamicCall.target, data, dynamicCall.value); | ||
| } | ||
|
|
||
| _emitOperationEvents(operation, proposal, intent.hash(), index, abi.encode(outputs)); | ||
| } | ||
|
|
||
| /** | ||
| * @dev Validates an intent and its corresponding proposal | ||
| The off-chain validators are assuring that: | ||
|
|
@@ -427,6 +499,22 @@ contract Settler is ISettler, Ownable, ReentrancyGuard, EIP712 { | |
| if (!smartAccountsHandler.isSmartAccount(user)) revert SettlerUserNotSmartAccount(user); | ||
| } | ||
|
|
||
| /** | ||
| * @dev Validates a dynamic call operation and its corresponding proposal | ||
| * @param operation Dynamic call operation to be fulfilled | ||
| * @param proposalData data of the proposal | ||
| * @param user The originator of the operation | ||
| */ | ||
| function _validateDynamicCallOperation( | ||
| DynamicCallOperation memory operation, | ||
| bytes memory proposalData, | ||
| address user | ||
| ) internal view { | ||
| if (operation.chainId != block.chainid) revert SettlerInvalidChain(block.chainid); | ||
| if (proposalData.length > 0) revert SettlerProposalDataNotEmpty(); | ||
| 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 | ||
|
|
@@ -556,6 +644,16 @@ contract Settler is ISettler, Ownable, ReentrancyGuard, EIP712 { | |
| emit OperationsValidatorSet(newOperationsValidator); | ||
| } | ||
|
|
||
| /** | ||
| * @dev Sets the dynamic call encoder | ||
| * @param newDynamicCallEncoder New dynamic call encoder to be set | ||
| */ | ||
| function _setDynamicCallEncoder(address newDynamicCallEncoder) internal { | ||
| if (newDynamicCallEncoder == address(0)) revert SettlerDynamicCallEncoderZero(); | ||
| dynamicCallEncoder = newDynamicCallEncoder; | ||
| emit DynamicCallEncoderSet(newDynamicCallEncoder); | ||
| } | ||
|
|
||
| /** | ||
| * @dev Sets a safeguard for a user | ||
| * @param user Address of the user to set the safeguard for | ||
|
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note:
bytes[]instead ofDynamicCall[]to avoid stack-too-deep error