Skip to content

Execute multiple contract calls atomically with optional sweep-on-failure#1702

Open
yrong wants to merge 4 commits intomainfrom
ron/multi-contract-calls
Open

Execute multiple contract calls atomically with optional sweep-on-failure#1702
yrong wants to merge 4 commits intomainfrom
ron/multi-contract-calls

Conversation

@yrong
Copy link
Contributor

@yrong yrong commented Feb 6, 2026

Summary

Adds optional sweep support to the CallContracts command so that when sub-calls fail, configured tokens (including ETH) can be swept to a recipient. This recovers assets that would otherwise be stuck in the agent when a call reverts.

Behavior

  • CallContracts: Runs sub-calls in order and reverts on the first failure.
  • Sweep on failure: When configured, a sweep runs in the catch block after a failure to move the agent’s ETH and/or ERC20s to a recipient.
  • Payload format: CallContractsParams bundles calls, sweepRecipient, and tokensToSweep.

@codecov
Copy link

codecov bot commented Feb 6, 2026

Codecov Report

❌ Patch coverage is 83.78378% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.14%. Comparing base (1832f1a) to head (6beeda5).

Files with missing lines Patch % Lines
contracts/src/AgentExecutor.sol 75.00% 3 Missing and 1 partial ⚠️
contracts/src/Gateway.sol 80.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1702      +/-   ##
==========================================
+ Coverage   81.04%   81.14%   +0.09%     
==========================================
  Files          22       22              
  Lines         997     1034      +37     
  Branches      186      195       +9     
==========================================
+ Hits          808      839      +31     
- Misses        172      177       +5     
- Partials       17       18       +1     
Flag Coverage Δ
solidity 81.14% <83.78%> (+0.09%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@yrong yrong changed the title Add multiple contract calls Call multiple contracts with optional Sweep-on-Failure Feb 9, 2026
@yrong yrong changed the title Call multiple contracts with optional Sweep-on-Failure Execute multiple contract calls atomically with optional sweep-on-failure Feb 15, 2026
external
onlySelf
{
CallContractsParams memory params = abi.decode(payload, (CallContractsParams));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Params are decoded here for a second time, can probably send CallContractsParams to trySweepOnFailure.

CallContractParams[] memory params = new CallContractParams[](1);
params[0] =
CallContractParams({target: address(0xdead), data: "", value: uint256(0)});
bytes memory payload = abi.encode(params);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
bytes memory payload = abi.encode(params);
CallContractsParams memory p = CallContractsParams({
calls: params,
sweepRecipient: address(0),
tokensToSweep: new address[](0)
});
bytes memory payload = abi.encode(p);

Since it should be a CallContractsParams struct, not CallContractParams[] array.

}

// Sweep remaining assets when specified
function sweep(address recipient, address[] calldata tokens) external {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can unrelated assets be swept unintentionally here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typically, the sweep token list is constructed by the SDK and kept consistent with the transfer token list.

By the way, the sweep flow is optional and can be skipped by providing an empty address.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I like this PR in theory. But I would like to build this at a higher level. So here we are mimicking the Asset Claimer behaviour by using (maybe abusing) Transact and the fact that we can implement anything via arbitrary contract call, rather than sticking close to the XCM functionality and build it into our protocol directly.

For instance we should probably stick closer to the XCM spec, and by default always sweep assets on any failure. By default we should sweep assets to the locations Agent, however if SetHints{Claimer} is set then then we use that account. The asset list should be filled in by the on-chain code from the XCM ground truth instead of expecting the sdk to build it correctly offchain. Also if a user has an agent, JIT create the Agent if it does not exist. Jit creating an agent and paying more gas, but having funds recoverable is probably better than failing and having funds trapped in limbo.

If a user didnt specify any claimer and the funds got sent to the agent, we could use ClaimAsset instruction to get them back for instance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants