My concern with the atomicity feature is that dapps will build things with this API that only work for the strict level of atomicity, which is bad for users of wallets that do not support it, i.e. the majority of Ethereum users today
And anything that is strict can probably be made into none by just architecting the contract differently–I can’t think of a counterexample to this point. So there’s no reason to actually require strict.
strict is also a bit funky when the combined calls require more than block gas limit and must be sent as multiple transactions. Maybe there should be an additional specification that the request should reject if the wallet cannot make all the calls in a single block, or maybe we want to support that use case of many calls that must be made in separate blocks
So those dapps will either struggle to get adoption from current Ethereum users, or build a fallback. I think it’s better to standardize these features so that dapps using them will automatically support wallets adding atomicity instead of relying on bespoke integrations.
Imagine I wrote a dapp today that needed strict atomicity, and I write code that supports Gnosis Safe using their API. I die and obviously am unable to continue updating my dapp. Some time passes, and we finally get EIP-3074. Now all wallets can implement support for strict atomicity, but my dapp is stuck only supporting Gnosis Safe.
For sure! I’m thinking of scenarios like complex DeFi arbitrage or rescuing NFTs from hacked accounts, where the transaction sender doesn’t have control over what they’re interacting with.
If a wallet can’t ensure the operations are performed atomically (because it’s an EOA, because of the gas limit, or for any other reason), the RPC call must fail.
In absence of the strict atomicity request parameter, wouldn’t you build your dapp and contracts to work for all wallets, i.e. using the default none behavior? In that case, all wallets would support it on day 0, and the UX for gnosis safe would be unchanged. The atomicity request parameter enables dapp developers to build dapps that are only supported by certain kinds of wallets, even though they could just build it for all wallets without sacrificing anything.
I think this is out of scope for this EIP, for a bunch of reasons in no particular order:
These use cases have more to do with using a private relay than bundling function calls
E.g. in cases where a rescue does require multiple function calls in a single transaction, and there is no authorization, you can do that in the constructor of a contract deployment (no bundling necessary)
This is a bespoke use case that is usually handled with scripting
The mempool and sequencing behavior is closely related to the underlying blockchain, whereas this API is meant to abstract those details away from the dapp developer as much as possible
The wallet can have a toggle to use a private relay without any change to the RPC interface
This does push a lot of complexity onto the dapp in the rare case that the set of function calls costs more than block gas limit. It’s nice for dapp developers to not have to worry about the total size of their batch. Wallets will also have different amounts of overhead for batch function calls, so dapps cannot predict the actual cost of sending the entire batch of function calls. But I wouldn’t be strongly opposed to adding a constraint that the RPC fails if it would be impossible to include all the function calls under the block gas limit.
I believe so. Even if a smart contract is not EOA-friendly, it can always be wrapped. For example, consider an ERC20 without an #approve function. To spend this token you would have to call #transfer and then atomically call another contract. You can work around this: deploy a contract to hold the tokens, transfer the tokens to that contract, and then execute calls from the holder contract.
For every example I can think of, there is a different pattern that does not require atomicity across the function calls. It might be possible to formally prove this point. Assuming the point is true, I don’t think there’s any reason for a dapp developer to build a feature that requires it, so the request parameter only acts as a footgun.
While it seems very likely that a contract that enforces strict atomicity can be constructed for every scenario, there’s room for dapps to use the strict flag, even if it does fail.
Dapp developers may want to optimistically enforce a strict bundle, and upon failure, ask the user to acknowledge the risk they will take by downgrading to ‘loose’ or no atomicity. If a Dapp presents a user with one button to click and sign multiple transactions, it seems like pretty poor UX if part of the call reverts. This is especially true for EOA users, whose current expectation is that each signature from their wallet results in one (atomic) transaction.
Instead of building an on-chain abstraction over every contract in the Dapp, developers can leverage account abstraction to create one-signature contract compositions for users (including for contracts that the dapp developer was unaware of, provided the user can import an ABI). Optimistically, the developer can request the bundle using "atomicity": "strict", then fall back to "loose" or "none" along with a warning and explanation that the atomic composition implied by the single signature for the queued transactions will be degraded.
Can you share an example of when you’d be ok with loose or no atomicity even though you wanted strict?
This case sounds like it’s not known by the dapp whether atomicity is needed or not, but that’s a really advanced user and the user should specify the atomicity requirement in this case… but wouldn’t they just not send these transactions separately if their wallet doesn’t support it or do some other workaround? Seems like a slightly convoluted use case AFAICT
The most concrete use case that came to mind was a block explorer with a transaction queue. If it provides users visiting with a way to visit a few contracts and build a set of transactions to execute, a naive user would probably expect the batch to execute together, especially with only one wallet signature for the full queue, and so the default behavior should be strict atomicity.
I agree that the ‘loose’ atomicity is a rather weak construction, as it encourages users to route batches to private mempools, and feels like an abstraction leak that may change with underlying things like AUTH/AUTHCALL or single-slot finality. I’d be happy to hear more arguments in it’s favor, but having a bollean flag for enforcing all-or-nothing execution of a bundle seems like a good abstraction of underlying wallet mechanics for developers of tools like block explorers or general purpose contract interfaces.
I’m operating under the assumption that as the EVM gets used by more people, competent early adopters will continue using EOAs, but new/less advanced users will onboard to accounts with features like atomic batching. I think we should make it easy for dapps to cater to them, and warn EOA users that “the bundle may only succeed in part or be split up, would you like to send the transactions anyway”.
Essentially I’d like the line in the EIP
MAY revert all calls if any call fails
to instead be
MUST revert all calls if any call fails when "atomic": "true"
For something like complex transaction building (e.g. furucombo) you usually want to involve a smart contract to introduce logic between the calls (e.g. all or nothing, these 2 calls are optional, take output of this call and pass as input to another call). Furucombo still greatly benefits from this EIP because the spending approvals can be batched with the function calls you want to make. The one drawback is the calls do not come from the user’s wallet. But I’m not sure if support for that is really required for any meaningful transaction building use case.
I’m on the fence. I want this interface to be as narrow and abstract as possible so as to prevent dapps from building branching logic based on wallet capabilities, which either adds work for the dapp developer or excludes wallets. The “atomic” flag definitely leaks wallet implementation details to the dapp. But if people are strongly in favor of this change I’ll add it–I’m definitely more ok with atomic flag than 3 different levels of atomicity which feels even leakier.
My two cents on this is I’d think early adopters will want account abstracted wallets for better security/recovery, or other features they offer. I myself am a user of a smart contract wallet, and many of my friends use gnosis safe for self custody. I’m not sure if that changes the calculus.
Yup, makes sense, totally get what’s being asked for here.
In the absence of this method, does it make sense to add one to the EIP just for checking if this particular set of methods is supported? Reviewing the guide posted by @marc and thinking about this again, I can’t imagine implementing this in a mature dapp and falling back to multiple eth_sendTransaction calls as I originally proposed. There will need to be a transitional period where a dapp needs to support both existing wallet APIs well and the function call bundle API.
Such a method could look like: wallet_isFunctionCallBundleSupported with no parameters, returns true if supported and errors or returns false if not supported.
I’d like to revive the discussion regarding the EIP as we continue our efforts to call for both wallet and dapps to integrate it.
Recently, we worked with PoolTogether to develop a more user-friendly interface for the no-loss donation use case. This involved a multi-step interaction process comprising 6 batch transactions, which included granting approval for the contract, depositing funds, and delegating winnings to another address. We didn’t encountered issues. Note that we built the frontend from the ground up to support exclusively batch transactions. There’s no way for an EOA to interact with it.
Net steps we will assist two dapps in integrating this EIP into their interfaces. Since these dapps already support the standard EOA workflow. We will be adding support for batch transactions if a smart wallet is connected. I will keep updates posted as they progress.
After Tweeting with only a vague recollection of this discussion, I accidentally discovered another axis I think should be covered by the endpoint: what happens on failure in the none case. Here are all the options now:
"atomicity": "continue-on-fail" → Send all the transactions even if some fail.
"atomicity": "stop-on-fail" → Stop on first failed operation. Operations successfully executed so far MUST NOT be reverted.
"atomicity": "loose-revert-on-fail" → If any operations fail, none of the operations should happen (they should either revert or not appear on-chain.) This could be implemented for EOAs with flashbots. May lose atomicity in the face of reorgs.
"atomicity": "strict-revert-on-fail" → If any operations fail, all operations MUST either revert or never appear on-chain. Must remain atomic in the face of reorgs.