SkillAgentSearch skills...

Code4rena

No description available

Install / Use

/learn @AmbireTech/Code4rena
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

🔥 Ambire Wallet 🔥

The first DeFi wallet that combines power, security and ease of use, while also being open-source and non-custodial.

Ambire Wallet

Useful links

Ambire contest details

Hello Wardens 👋

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!

Contest scope

All the contracts in contracts/, namely Identity.sol, libs/SignatureValidatorV2.sol, libs/BytesLib.sol, IdentityFactory.sol, wallet/QuickAccManager.sol, wallet/Zapper.sol - that's a total of 772 lines.

Architecture

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 Identity(account).execute

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 Identity.isValidSignature uses 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.

NOTE: The UI is currently in private beta, but you can use the factory contract and Identity.sol to experiment on Polygon mainnet.

Design decisions

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

Identity.sol

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 msg.sender is authorized

There's 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 execute/executeBySender, ensuring it's authorized. Those methods are setAddrPrivilege, tipMiner and tryCatch.

It's only dependency is an internal one, SignatureValidatorV2.

SignatureValidatorV2.sol

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).

IdentityFactory.sol

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.

wallet/QuickAccManager.sol

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 Identity

And two EIP 712 methods: sendTransfer and sendTxns, which only allow 2/2 signatures, but support typed data signatures.

wallet/Zapper.sol

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.

Known tradeoffs

NOTE: "bundle"/"user bundle" in this context means array of Identity-level transactions (Identity.Transaction[])

  • 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 bytes32 for the privileges mapping and treat any nonzero value as true. This is because we utilize the storage space for periphery contracts such as QuickAccManager or a planned MultiSigManager in the future. Utilizing a storage slot has the same gas costs no matter if true or 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 se

Related Skills

View on GitHub
GitHub Stars6
CategoryDevelopment
Updated1y ago
Forks4

Languages

Solidity

Security Score

50/100

Audited on Dec 12, 2024

No findings