# Writing an ERC-6900 module to do round up savings with ERC20s
In this post, we willl walk through the steps of creating an ERC-6900 compliant smart contract plugin that saves a little bit everytime you spend ERC-20 from your wallet. If you just care about the code, you can find it [here](https://github.com/locker-labs/savings-module).
The goal is to create an experience similar to Acorns.com's round-up savings, but completely on-chain and ERC-4337 native.
For the purposes of this blog post, the terms 'plugin', 'module', and 'extension' are all used interchangeably.
## A tale of two standards
ERC-4337 smart wallets are evolving to allow for plugins to be installed as easily as apps onto your phone. Recent standards like [ERC-6900](https://eips.ethereum.org/EIPS/eip-6900) and [ERC-7579](https://eips.ethereum.org/EIPS/eip-7579) provide ways to extend wallet functionality with plugins.
ZeroDev is one of the authors of 7579 but does a good job comparing the two standards in [this](https://docs.zerodev.app/blog/who-when-what) blog post. The basic summary is that 6900 is more opinionated and portable. Whereas 7579 is less perscriptive and more composable.
Although the technical discussion around 7579 and 6900 are interesting, [Locker](https://locker.money) is starting with 6900 for other reasons. We are working on this plugin in partnership with a consumer wallet that is using Alchemy AA Kit. Alchemy AA only supports 6900. Similarly, Locker is a current Circle grantee. Circle also exclusively [supports 6900](https://www.circle.com/modules-beta) for their smart wallets.
### ERC-6900 basics
This code is based off an [intro to ERC-6900](https://www.youtube.com/watch?v=vvjJLz1atP4) series from Alchemy. That tutorial and the other resources below are good for getting an introduction to 6900.
- [Alchemy 6900 guide](https://www.notion.so/alchemotion/How-to-write-an-ERC-6900-Plugin-8ef518630b1a43a1b301723925407ec5)
- [Alchemy subscriptions tutorial](https://docs.alchemy.com/alchemy/subscription)
- [Reference implementation](https://github.com/erc6900/reference-implementation)
- [Demystifying 6900](https://medium.com/decipher-media/demystifying-erc-6900-5a52db06dcff)
## Writing the plugin
The code for the plugin is contained within a single file and is composed of three main sections:
1. State management
2. Execution logic
3. Configuring the plugin
### 1. State Management
Plugins in ERC-6900 are singletons, meaning that all smart accounts using the plugin share the same instance. Therefore, it’s essential to design the singleton to keep track of each account’s individual settings.
In our case, we want a single wallet to support multiple savings automations. First, we define a `SavingsAutomation` struct to hold details for one round-up record (such as the amount to save and the recipient of the savings). Then, we create a mapping that allows multiple automations for a single user.
```solidity
struct SavingsAutomation {
address savingsAccount; // where to send the funds
uint256 roundUpTo; // e.g., 1,000,000 for a USD stablecoin with 6 decimals represents 1 USD
bool enabled;
}
// Every owner address can have multiple automated savings configured
mapping(address => mapping(uint256 => SavingsAutomation))
public savingsAutomations;
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ Execution functions ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
function createAutomation(
uint256 automationIndex,
address savingsAccount,
uint256 roundUpTo
) external {
savingsAutomations[msg.sender][automationIndex] = SavingsAutomation(
savingsAccount,
roundUpTo,
true
);
}
```
### 2. Execution Logic
The logic for this plugin needs to execute whenever the parent wallet initiates an ERC-20 token transfer. ERC-6900 exposes two hooks: `preExecutionHook` and `postExecutionHook`. Ideally, we would use `postExecutionHook` for this case to perform the round-up savings after the original ERC-20 transfer. However, `postExecutionHook` also requires an implementation of `preExecutionHook`. For simplicity in this tutorial, we placed all logic within `preExecutionHook`.
The main steps in this function involve decoding the transaction data, calculating the savings amount based on the specified round-up, and initiating an ERC-20 transfer from the smart account via `executeFromPluginExternal`. Here’s the core logic:
```solidity
function preExecutionHook(
uint8 functionId,
address,
uint256 value,
bytes calldata data
) external override returns (bytes memory) {
...
IPluginExecutor(msg.sender).executeFromPluginExternal(
tokenAddress,
0, // No ETH required
abi.encodeWithSelector(
IERC20.transfer.selector,
automation.savingsAccount,
savingsAmount
)
);
}
```
### 3. Configuring the plugin
One of the things that distinguishies 6900 from 7579 is the manifest. The manifest is inspired by the Android app manifest and defines plugin permissions and dependencies.
Here are the important bits for setting up a hook:
```solidity
function pluginManifest()
external
pure
override
returns (PluginManifest memory)
{
PluginManifest memory manifest;
...
// Register post-execution hooks on ERC20 transfers
manifest.executionHooks = new ManifestExecutionHook[](1);
ManifestFunction memory none = ManifestFunction({
functionType: ManifestAssociatedFunctionType.NONE,
functionId: 0,
dependencyIndex: 0
});
ManifestFunction memory execHook = ManifestFunction({
functionType: ManifestAssociatedFunctionType.SELF,
functionId: uint8(FunctionId.EXECUTE_FUNCTION),
dependencyIndex: 0
});
manifest.executionHooks[0] = ManifestExecutionHook({
executionSelector: IStandardExecutor.execute.selector,
preExecHook: execHook,
postExecHook: none
});
manifest.permitAnyExternalAddress = true;
manifest.canSpendNativeToken = true;
}
```
### Gotcha: How ERC-4337 Accounts Send ERC-20 Tokens
The v0.7.x of the [6900 reference implementation](https://github.com/erc6900/reference-implementation/blob/8b9ba71c25c7fd322da26889cc2a9c150eda2869/src/account/UpgradeableModularAccount.sol) requires, sending ERC-20 tokens from a smart account via the `execute` command rather than directly encoding an ERC-20 transfer in the calldata.
Here’s how to structure the `UserOperation` the right way.
```solidity
UserOperation memory userOp = UserOperation({
sender: account1Address,
nonce: 1,
initCode: "",
callData: abi.encodeWithSelector(
IStandardExecutor(account1Address).execute.selector,
testTokenAddress, // target address (ERC20 token)
0, // no ETH required
abi.encodeWithSelector( // data parameter (calls ERC20's `transfer`)
IERC20(testTokenAddress).transfer.selector,
address(paymentRecipient),
sendAmount
)
),
callGasLimit: CALL_GAS_LIMIT,
verificationGasLimit: VERIFICATION_GAS_LIMIT,
preVerificationGas: 0,
maxFeePerGas: 2,
maxPriorityFeePerGas: 1,
paymasterAndData: "",
signature: ""
});
```
## What’s Next
Several areas need further testing and refinement for this plugin:
- Ensuring that the round-up savings transfer only occurs if it doesn’t cause the main ERC-20 transfer to fail
- Testing edge cases, such as when the transfer amount matches or exceeds the `roundUpTo` threshold
- Supporting multiple savings automations for a single account, as well as handling multiple automations across different accounts
- Verifying that the main ERC-20 transfer still succeeds, even if the savings transfer fails
For developers interested in implementing a similar feature using ERC-7579 instead of ERC-6900, [Rhinestone's example](https://github.com/rhinestonewtf/modulekit-examples/tree/main/src/auto-savings) provides a good starting point.