🔥 Ambire Wallet 🔥
The first DeFi wallet that combines power, security and ease of use, while also being open-source and non-custodial.
Ambire contest details
- $33,750 USDC main award pot
- $1,250 USDC gas optimization award pot
- Join the C4 Discord to register
- Submit findings using the C4 form
- Read our guidelines for more details
- Starts October 15, 2021 00:00 UTC
- Ends October 18, 2021 23:59 UTC
We are looking forward to you diving into our code!
Feel free to ask us anything you want, no matter if it's a minor nitpick or a severe issue. We remain available around the clock in the Code4rena Discord, and don't hestitate to tag @Ivo#8114.
Good luck and enjoy hunting!
We hope you're excited about finally seeing a usable and powerful smart wallet on Ethereum!
All the contracts in
wallet/Zapper.sol - that's a total of 772 lines.
Ambire is a smart wallet. Each user is represented by a smart contract, which is a minimal proxy (EIP 1167) for
Identity.sol (example) - we call "account". Many addresses can control each account - we call this "privileges" in the contract and "authorities" in the UI.
The main contract everything is centered around is
Identity.sol, which is the actual smart wallet.
Accounts can execute multiple calls in the same on-chain transaction. We call the array of user transactions a "user bundle" - the user signs the hash of this array along with anti-replay data such as nonce, chainID and others. Once it's signed, anyone can execute it by calling
The addresses that control an account (privileges) can be EOAs but they can also be smart contracts themselves, thanks to the
SmartWallet signature mode in
SignatureValidatorV2 which enables EIP 1271 signatures to be used.
To allow more sophisticated authentication schemes without upgradability, we use a very simple relationship: a periphery contract that only deals with the specific authentication scheme can be added to
privileges. For example, if a user wants to convert their account to a multisig, they can remove all other privileges and only authorize a single one: a multisig manager contract, that will verify N/M signatures and call
Identity(account).executeBySender upon successful verification. This also works for EIP 1271 signatures since
SignatureValidatorV2, which supports EIP 1271 itself, so it will propagate the call down to the multisig manager contract.
This very system is used by
QuickAccManager, which is a simple 2/2 multisig that also allows 1/2 transactions but with a timelock. This is used to allow for simple email/password login that can be upgraded by either backing up the second key or by moving to a hardware wallet. For more info on this authentication scheme please read the security model Gist.
There are two ways for a user bundle to get executed:
- Directly, when a user's EOA pays for gas
- Through a Relayer that takes the signed message that authorizes a user bundle, and broadcasts it itself, paying for gas. The user bundle will have to contain an ERC20 transaction that pays the Relayer to reimburse it for gas. Currently we have a proprietary relayer that does all of this.
The actual proxy for each account is deployed counterfactually, when the first user bundle is executed.
Because user bundles are authorized as signed messages, there's no need for hardware wallets to support EIP 1559 directly.
Similar products include Argent, Gnosis Safe and Authereum. The most notable differences is that the Ambire contracts are designed to be as simple as possible, and prefer composability to upgradability and built-in modularity.
Testing and JS libs
The contracts in scope can also be found in this repo: https://github.com/AmbireTech/adex-protocol-eth/tree/identity-v5.2, specifically the identity-v5.2 branch (NOTE: we only care about the contracts in scope!).
The code is frozen for review on commit 742e800c5a6aabe08c59625f3dfd85139223ee63.
In the repo, there are also tests that can be ran, namely
test/TestIdentity.js. Other pieces of code you need to know about are
js/Bundle.js, responsible for preparing and signing user bundles, and
js/IdentityProxyDeploy.js, responsible for deploying the minimal proxies.
The contracts are free of inheritance and external dependencies.
There is no code upgradability and no ownership (
onlyOwner) or pausability, to ensure immutability. For easier readability, there are no modifiers, while keeping the code DRY.
Storage usage is cut down to the minimum: when bigger data structures need to be saved, we take advantage of the cheap calldata and always pass them in, verifying the hash against a storage slot in the process, for example
QuickAccManager uses this for quick accounts.
Smart contract summary
The core of the Ambire smart wallet. Each user is a minimal proxy with this contract as a base. It contains very few methods, with the most notable being:
execute: executes a signed user bundle
executeBySender: executes a bundle as long as
There are a few methods that can only be called by the Identity itself, which means the only way to call them is through a call through
executeBySender, ensuring it's authorized. Those methods are
It's only dependency is an internal one,
Validates signatures in a few modes: EIP 712, EthSign, SmartWallet and Spoof. The first two verify signed messages using
ecrecover, the only difference being that EthSign expects the "Ethereum signed message:" prefix. SmartWallet is for ERC 1271 signatures (smart contract signatures), and Spoof is for spoofed signatures that only work when
tx.origin == address(1).
A simple CREATE2 factory contract designed to deploy minimal proxies for users. The most notable point here is
deploySafe, which is a method that protects us from griefing conditions:
CREATE2 will fail if a contract has already been deployed, and this method essentially ensures a contract is deployed without failing if it already is.
The use case of this is counterfactual deployment: the proxy of each account will be deployed when the first user bundle is executed, but we don't want to fail the whole bundle in case the contract has already been deployed.
There is a method to drain the contract of ERC20 tokens.
This contract facilitates a 2/2 multisig scheme described in the aforementioned security model document.
It has a set of methods for sending, scheduling and cancelling user bundles:
send: will execute a bundle immediately if it has 2/2 signatures, or schedule it if it's 1/2
cancel: will cancel any pending bundle
execScheduled: will execute a matured scheduled bundle as long as the QuickAcc is still authorized on the
And two EIP 712 methods:
sendTxns, which only allow 2/2 signatures, but support typed data signatures.
This contract routes trades for users through any Uniswap V2 or V3 compatible router, and facilitates deposit/withdraw to/from Aave.
Unlike regular contracts that use
transferFrom, this one relies on the fact that Ambire accounts can execute multiple calls in a single on-chain transaction with user bundles, so it expects that you just
transfer-ed the tokens to it beforehand.
NOTE: "bundle"/"user bundle" in this context means array of Identity-level transactions (
- QuickAccManager security model: QuickAccManager allows users to control their wallets through a 2/2 multisig (see security model), with one of the keys in their own custody and the other key on the Ambire Relayer, with a possibility of the user backing it up. Timelocked transactions can be sent or cancelled by only 1/2 keys. This means that if the Ambire key is compromised AND lost, the attacker can cause grief by cancelling every attempt of the user to recover their funds. This can be avoided if the user backs up their key, which we recommend anyway for guaranteed full custody.
- Storing additional data in
privileges: instead of boolean values, we use
privilegesmapping and treat any nonzero value as
true. This is because we utilize the storage space for periphery contracts such as
QuickAccManageror a planned
MultiSigManagerin the future. Utilizing a storage slot has the same gas costs no matter if
trueor hash is stored.
- ERC20 fees taken through the transaction batch: there's no special mechanism for reimbursing the relayer for the gas fee. Instead, the relayer looks at the bundle (
Transactions) and sees if one or more of those transactions are ERC20
transfers that send tokens to it. The relayer is responsible for checking whether the fee token and amount is acceptable for it, as well as checking it the transaction will execute before broadcasting it to the mempool. This is also a tradeoff cause the internal transactions may fail, in which case the whole bundle reverts and the fee is not paid, but the relayer will pay for gas. This is worked around on the Relayer end by utilizing Flashbots and Eden to avoid mining failing transactions, and by simulating the transactions right before trying to mine them. The reason we don't try/catch the errors int he
Identityis because we want user bundles to succeed/fail as a whole (atomically), and the transaction to show as failing on Etherscan.
- Signature spoof mode: the
SignatureValidatorV2.solcontract has a mode which allows signatures to be spoofed. The purpose of this is to allow easier simulation through
eth_estimateGasbefore having a signature from the user, since without this we would have a cyclical dependency that takes two steps to resolve (fee is unknown, user signs once to estimate the fee, then user signs a second time cause the bundle changed). This spoofing should not be allowed when calling through anywhere else other than
Identity(account).execute, and it only works if
tx.origin == address(1).
- Zapper approvals: The
Zappercontract does not require any ERC20 approvals, because we utilize the fact that users can batch transactions to transfer tokens to it and then do the trades in oen transaction, atomically. This saves some gas. This also means there are no
transferFromcalls in the
Zapper, as it just assumes it should have the tokens sent to it beforehand.
- QuickAccManager bricking: an account may be bricked by changing the
privileges[quickAccManager]entry to a different value; this is something that has to be tackled off-chain, and it's a fundamental issue of such contracts in general
How to run the tests
The contracts will be deployed on Ethereum, Polygon, Fantom, Binance Smart Chain, Avalanche, Arbitrum and other popular EVM chains.
If you're excited about building an easy to use but powerful smart wallet, feel free to reach out at email@example.com