Skip to main content

3 posts tagged with "wallet"

View All Tags

· 6 min read

candide_stack

Candide Wallet is an open-source, secure, self-custodial Smart Wallet built on ERC-4337. It leverages account abstraction without compromising on decentralization and censorship resistance. In practical terms, this means a Smart Wallet that doesn't depend on a centralized relayer controlled by a single entity. A gateway to undeniable free access to Ethereum for anyone with a smartphone and internet access.

Candide Wallet Mobile App is powered by a vertically integrated open-source technology stack that delivers an unmatched digital wallet user experience. We highlight the underlying technology we used to build it out. We also explore the reasons those technologies were chosen. Building on the shoulder of giants as we make use of many existing open-source libraries, as well as rolling our own.

The Contracts

ERC-4337 EntryPoint

The EntryPoint is the global contract for ERC-4337 Smart Accounts and Paymasters. It coordinates the verification and execution of a UserOperation. The entrypoint contract is audited by OpenZepplin and is used by Candide Wallet. The EntryPoint contract has been deployed on multiple chains, in the same address for Ethereum, Polygon, Optimism, Gnosis and other supported networks.

Candide Wallet Contracts

Candide Wallet Contracts leverage Safe Contracts to be ERC-4337 compatible and efficient. The Contracts implements a Smart Account with 2 essential methods for the EntryPoint to interact with it: validateUserOp and execute. They also have 2 more methods for both setup and replacing the EntryPoint for upgradability. Learn more about the contracts in this previous blog post.

Candide Paymaster Contracts

Paymasters are smart contract accounts that can sponsor transactions for Smart Accounts, enabling fee abstraction for dapps and wallets. Paymaster Sponsorship Options include:

  • Gasless - Pay gas fees on behalf of your users, essentially making transactions Gassless
  • Subsidized - Pay partial gas fees for your users, letting them pay only a discounted gas price
  • Paid with ERC-20 - Allows your users to pay gas with your native ERC-20 token

Paymaster Contracts can be customized to allow for only certain methods, minimum deposits or certain lock durations. In addition, users don't have to worry about gas limits or gas prices since the Paymaster automatically reprices transactions to ensure prompt execution.

Candide Modules (Plug-ins)

  • Account Recovery - Candide Wallet implements an on-chain account recovery module. This powers the wallet's seedless experience. In the case of the single-owner account, the signer key is typically stored on the user's device. More specifically, an owner can add recovery methods (also known as Guardians) to change the ownership of the account, in case their signer key is lost or compromised. Learn more about the recovery module in this previous blog post.

Candide plans to develop more modules in the upcoming weeks and months ahead. Some of the most exciting modules that we also plan to build are Session Keys and Subscriptions.

  • Session Keys - allow users to preapprove an application's transactions according to a set of parameters; time, gas usage, set transaction volumes of a token(s), or particular functions on specific contracts. This unlocks a better user experience for apps that leverages session keys.

  • Subscription - While recurring payments are an important primitive of credit card systems and an important tool for businesses, digital memberships can be reimagined from the ground up using contracts behind the scenes. Subscriptions allow Operators to set rules for how payments are collected, how installments are paid, who gets access, and under what conditions by combining it with NFTs.

Voltaire - Candide's 4337 Bundler

Voltaire is a modular, developer-friendly and lighting-fast Python Bundler for Ethereum EIP-4337 Account Abstraction. Voltaire is designed to operate within the peer-to-peer mempool of UserOperations, facilitating the inclusion of transactions on-chain more quickly and efficiently. A good way to think about Voltaire is that it is a software that delivers User Operations but cannot change them, similar to those expected of block-builders. It is incentivized to participate in the network through gas fees and MEV.

Voltaire is compliant with Infinitism Compatibility Test Suite. These test suites ensure that different bundlers can communicate effectively and defend against DDoS attack vectors.

The bundler is open-source, meaning that anyone can launch Voltaire and capture User Operation profits. By participating in the network, the mempool becomes more robust when facing censorship attacks.

Wallet Dart - SDK for Smart Accounts

Wallet Dart SDK is Candide's set of tools for creating, managing and engaging with ERC-4337 Smart Accounts in mobile client applications. The library's focus on smart contract account architecture gives several advantages to build for cross-platform mobile-friendly wallets. It provides pre-built functions and utilities that allow you to seamlessly interact with Smart Accounts securely and efficiently. Dart is one of the most performant cross-platform languages around; enabling the greatest productivity, with the fewest lines of code.

Benefits of using Wallet Dart

Using Wallet Dart provides several benefits, including:

  • Mobile First Smart Accounts** - Easily generate a new Smart Account associated with a user signer. Encrypt and Decrypt the signer with an easy to use open source library
  • ERC-4337 compatible - Directly integrate with Candide architecture without diving too deep into how the Entrypoint works under the hood
  • Send User Operations - Send transactions, including ERC20 and NFT transfers and interaction with arbitrary smart contracts, through Voltaire Bundler
  • Account Recovery - Offer a recovery mechansm that helps your users access their account without a seed-phrase
  • Enhanced UX - Candide Paymaster support gasless transactions, improving the user experience and making it more seamless to interact.
  • Better dev experience - The Wallet Dart SDK abstracts away the complexities of the development Smart Accounts, such as cryptography, wallet management, and smart contract interactions, making it easier to build account abstraction applications

Use the Wallet Dart Library and contribute back. More documentation coming soon. Hop on our Discord if you need any help.

Optimism

Candide Wallet is powered by the OP Stack, the open-source development stack that powers Optimism. An Ethereum layer 2 that improves the scalability and usability of crypto, bringing lower cost gas coupled with lighting fast transactions.

Modile Client

Candide Mobile Wallet is a cross-platform, mobile first, built with Flutter. Candide Mobile App seamless mobile experiences targets end-users on any mobile device. It is not only built on top of not only the libraries mentioned above but also on top of countless open-source code that was published for free to make it happen. You can get our Beta builds for both Android and iOS on our Discord, or by building it yourself on our github.

· 13 min read

account-recovery-device-screens

Securing private keys is a major challenge in making Ethereum wallets accessible to everyone. With the constant threat of phishing and targeted attacks, how do we make it easy to manage private keys, and prevent them from being lost and stolen? By abstracting away seed phrases and introducing an account recovery module, Ethereum wallets could reach the same level of security as a multisig wallet, making it easier for both Ethereum pros and the average joe to manage their account security. This model of security has been highlighted in Vitalik's blog post.

How it works

We introduce a new Account Recovery module for Candide Wallet. The Account Recovery module is designed to work for both a single-owner account and an N by M multi-sig account. In the case of the single-owner account, the signer key is typically stored on the user's device. More specifically, an owner can add recovery methods (also known as Guardians) to change the ownership of the account, in case their signer key is lost or compromised.

Recovery Contacts

Recovery methods are typical Ethereum accounts. They can be:

  • A family member or a close friend with Ethereum Account
  • Hardware wallets
  • Institutions
drawing

Normal operations of the Account do not require the approval of any Recovery Contacts in the module. When a user loses access to their wallet, they can reach out to their recovery contacts or methods to sign a transaction to recover their account.

The owner of the account decides the threshold for the number of guardians needed for recovery, as well as the number of guardians. A typical single-owner account can have 3 guardians with a threshold of 2. This increases the likelihood that a single guardian can overtake the account. By default, Candide Wallet makes the threshold the majority (> 50%), making the experience more straightforward on its interface.

Privacy in mind

Owners are encouraged to add fresh recovery addresses. This makes them private and eliminates the possibility of malicious contacts cooperating against an owner. By design, a recovery contact does not need to necessarily store value in their account to maintain their duties, even during a recovery process. We explore this design below in detail.

Recovery Delay Period

A recovery period for an account is a period during which the ownership transfer is delayed after all recovery contacts confirm their signatures. This delay period is intended to provide users with additional protection against the risk of malicious guardians overtaking the account. During the delay period, the owner of the account can cancel a recovery in the process if they still own the account. The length of the period can vary. Candide Wallet by default offers a fixed grace period of several days.

Censorship Resistant

The owner can remove and replace a recovery contact at any time

drawing

Account Recovery interfaces can be built with or without a backend service:

  • Without a backend: Each recovery contact can submit their signature transactions separately on the blockchain. Once the threshold is meant, anyone can call execute recovery to start the grace period.

  • With a backend: This service can aggregate guardians' signatures so that only the last guardian executes the transaction and pay gas fees. This is similar to how Safe's interface works when multiple owners for a multisig sign transactions before submitting them.

Highlevel Spec

We assume that the signer key belongs to its real owner. The probability of the signer key being in control of someone else should be close to zero. Under this model, we can build a simple yet highly secure non-custodial wallet. To enable that model to evolve if needed, upgrading the wallet to a new implementation requires the approval of only the owner of the account.

MethodOwnerGuardiansAnyoneComment
addGuardianWithThresholdXOwner can add a guardian with a new threshold
revokeGuardianWithThresholdXOwner can remove a guardian from its list of guardians
confirmRecoveryXLets a single guardian approve the execution of the recovery request
multiConfirmRecoveryXLets multiple guardians approve the execution of the recovery request
cancelRecoveryXLets an owner cancel an ongoing recovery request
finalizeRecoveryXFinalizes an ongoing recovery request if the recovery period is over. The method is public and callable by anyone

The Contract

The Account Recovery module is a smart contracts contract that gets added to the main Wallet Contract. Since Candide Wallet is based on Safe, they implement the Account functionality while separating module logic from the Account core contract. Adding and removing a module requires confirmation from the owner. Modules are security-critical, so they need to be as secure as all other contracts. Events are emitted whenever a module is added or removed and whenever a module transaction was successful or failed.

We will be using today's lingo Guardians in our code and explanation to reference a Recovery Contact or Device.

Add a Guardian

Lets an owner add a guardian for their wallet at any point in time. They also specify the new threshold for recovery after the addition

/social_recovery/SocialRecoveryModule.sol
/**
* @notice Lets the owner add a guardian for its wallet.
* @param _wallet The target wallet.
* @param _guardian The guardian to add.
* @param _threshold The new threshold that will be set after addition.
*/
function addGuardianWithThreshold(address _wallet, address _guardian, uint256 _threshold) external authorized(_wallet) {
guardianStorage.addGuardian(_wallet, _guardian);
guardianStorage.changeThreshold(_wallet, _threshold);
emit GuardianAdded(_wallet, _guardian, _threshold);
}
/social_recovery/storage/GuardianStorage.sol
function addGuardian(address _wallet, address _guardian) external onlyModule(_wallet) {
require(_guardian != address(0) && _guardian != SENTINEL_GUARDIANS && _guardian != _wallet, "GS: invalid guardian");
require(!Safe(payable(_wallet)).isOwner(_guardian), "GS: guardian cannot be an owner");
GuardianStorageEntry storage entry = entries[_wallet];
require(entry.guardians[_guardian] == address(0), "GS: duplicate guardian");
if (entry.count == 0){
entry.guardians[SENTINEL_GUARDIANS] = _guardian;
entry.guardians[_guardian] = SENTINEL_GUARDIANS;
}else{
entry.guardians[_guardian] = entry.guardians[SENTINEL_GUARDIANS];
entry.guardians[SENTINEL_GUARDIANS] = _guardian;
}
entry.count++;
}

function changeThreshold(address _wallet, uint256 _threshold) external onlyModule(_wallet) {
GuardianStorageEntry storage entry = entries[_wallet];
// Validate that threshold is smaller than or equal to number of guardians.
require(_threshold <= entry.count, "GS: threshold must be lower or equal to guardians count");
if (entry.count == 0){
require(_threshold == 0, "GS: threshold must be 0");
}else{
require(_threshold > 0, "GS: threshold cannot be 0");
}
entry.threshold = _threshold;
}

Remove a Guardian

Lets an owner remove a guardian for their wallet at any point in time. Similarly, They also specify the new threshold for recovery after the addition

/social_recovery/SocialRecoveryModule.sol
/**
* @notice Lets the owner revoke a guardian from its wallet.
* @param _wallet The target wallet.
* @param _prevGuardian The previous guardian linking to the guardian in the linked list.
* @param _guardian The guardian to revoke.
* @param _threshold The new threshold that will be set after execution of revokation.
*/
function revokeGuardianWithThreshold(address _wallet, address _prevGuardian, address _guardian, uint256 _threshold) external authorized(_wallet) {
require(isGuardian(_wallet, _guardian), "SM: must be existing guardian");
uint256 _guardiansCount = guardianStorage.guardiansCount(_wallet);
require(_guardiansCount - 1 >= _threshold, "SM: invalid threshold");
guardianStorage.revokeGuardian(_wallet, _prevGuardian, _guardian);
guardianStorage.changeThreshold(_wallet, _threshold);
emit GuardianRevoked(_wallet, _guardian, _threshold);
}
/social_recovery/storage/GuardianStorage.sol
function revokeGuardian(address _wallet, address _prevGuardian, address _guardian) external onlyModule(_wallet) {
GuardianStorageEntry storage entry = entries[_wallet];
require(_guardian != address(0) && _guardian != SENTINEL_GUARDIANS, "GS: invalid guardian");
require(entry.guardians[_prevGuardian] == _guardian, "GS: invalid previous guardian");
entry.guardians[_prevGuardian] = entry.guardians[_guardian];
entry.guardians[_guardian] = address(0);
entry.count--;
}

Execute Recovery

This method lets anyone execute the recovery if the number of thresholds for guardians has been met. Once executed, a grace period is started before anyone can call the finalization method.

/social_recovery/SocialRecoveryModule.sol
function executeRecovery(address _wallet, address[] calldata _newOwners, uint256 _newThreshold) external {
uint256 guardiansThreshold = threshold(_wallet);
require(guardiansThreshold > 0, "SM: empty guardians");
//
uint256 _approvalCount = getRecoveryApprovals(_wallet, _newOwners, _newThreshold);
require(_approvalCount >= guardiansThreshold, "SM: confirmed signatures less than threshold");
_executeRecovery(_wallet, _newOwners, _newThreshold, _approvalCount);
}
/social_recovery/SocialRecoveryModule.sol
/**
* @notice Lets the guardians start the execution of the recovery request.
* Once triggered the recovery is pending for the recovery period before it can be finalised.
* @param _wallet The target wallet.
* @param _newOwners The new owners' addressess.
* @param _newThreshold The new threshold for the safe.
* @param _approvalCount The collected (confirmed) guardians signatures for this recovery operation.
*/
function _executeRecovery(address _wallet, address[] calldata _newOwners, uint256 _newThreshold, uint256 _approvalCount) internal {
uint256 _nonce = nonce(_wallet);
// If an ongoing recovery exists, replace only if more guardians than the previous guardians have approved this replacement
RecoveryRequest storage request = recoveryRequests[_wallet];
if (request.executeAfter > 0){
require(_approvalCount > request.guardiansApprovalCount, "SM: not enough approvals for replacement");
delete recoveryRequests[_wallet];
emit RecoveryCanceled(_wallet, _nonce - 1);
}
// Start recovery execution
uint64 executeAfter = uint64(block.timestamp + recoveryPeriod);
recoveryRequests[_wallet] = RecoveryRequest(_approvalCount, _newThreshold, executeAfter, _newOwners);
walletsNonces[_wallet]++;
emit RecoveryExecuted(_wallet, _newOwners, _newThreshold, _nonce, executeAfter, _approvalCount);
}

Validate Guardian Signatures

Validates the provided signature to check if the guardian is the correct one added by the owner of the account

/social_recovery/SocialRecoveryModule.sol
/// @dev checks if valid signature to the provided signer, and if this signer is indeed a guardian, revert otherwise
function validateGuardianSignature(
address _wallet,
bytes32 _signHash,
address _signer,
bytes memory _signature
) public view {
require(isGuardian(_wallet, _signer), "SM: Signer not a guardian");
require(SignatureChecker.isValidSignatureNow(_signer, _signHash, _signature), "SM: Invalid guardian signature");
}

Confirm Recovery

This method lets a single guardian confirm the execution of the recovery request. Notice the _execute, when passed, allows the guardian to both sign and execute the recovery. THis is useful when building an interface for a seamless one-click experience.

/social_recovery/SocialRecoveryModule.sol
/**
* @notice Lets single guardian confirm the execution of the recovery request.
* Can also trigger the start of the execution by passing true to '_execute' parameter.
* Once triggered the recovery is pending for the recovery period before it can be finalised.
* @param _wallet The target wallet.
* @param _newOwners The new owners' addressess.
* @param _newThreshold The new threshold for the safe.
* @param _execute Whether to auto-start execution of recovery.
*/
function confirmRecovery(address _wallet, address[] calldata _newOwners, uint256 _newThreshold, bool _execute) external {
require(isGuardian(_wallet, msg.sender), "SM: sender not a guardian");
require(_newOwners.length > 0, "SM: owners cannot be empty");
require(_newThreshold > 0 && _newOwners.length >= _newThreshold, "SM: invalid new threshold");
//
uint256 _nonce = nonce(_wallet);
bytes32 recoveryHash = keccak256(encodeRecoveryData(_wallet, _newOwners, _newThreshold, _nonce));
confirmedHashes[recoveryHash][msg.sender] = true;
//
if (!_execute) return;
uint256 guardiansThreshold = threshold(_wallet);
uint256 _approvalCount = getRecoveryApprovals(_wallet, _newOwners, _newThreshold);
require(_approvalCount >= guardiansThreshold, "SM: confirmed signatures less than threshold");
_executeRecovery(_wallet, _newOwners, _newThreshold, _approvalCount);
}

Multi-Confirm Recovery

This method lets multiple guardians confirm the execution of recovery at the same time. It aggregates all their signature into a SignatureData[] array and submits them at once. This method is useful to allow for only one guardian to pay tx fees instead of all guardians submitting their signatures one by one. Similarly, the _execute param, when passed, allows for triggering the execution as well.

/social_recovery/SocialRecoveryModule.sol
/**
* @notice Lets multiple guardians confirm the execution of the recovery request.
* Can also trigger the start of the execution by passing true to '_execute' parameter.
* Once triggered the recovery is pending for the recovery period before it can be finalised.
* @param _wallet The target wallet.
* @param _newOwners The new owners' addressess.
* @param _newThreshold The new threshold for the safe.
* @param _signatures The guardians signatures.
* @param _execute Whether to auto-start execution of recovery.
*/
function multiConfirmRecovery(address _wallet, address[] calldata _newOwners, uint256 _newThreshold, SignatureData[] memory _signatures, bool _execute) external {
require(_newOwners.length > 0, "SM: owners cannot be empty");
require(_newThreshold > 0 && _newOwners.length >= _newThreshold, "SM: invalid new threshold");
require(_signatures.length > 0, "SM: empty signatures");
uint256 guardiansThreshold = threshold(_wallet);
require(guardiansThreshold > 0, "SM: empty guardians");
//
uint256 _nonce = nonce(_wallet);
bytes32 recoveryHash = keccak256(encodeRecoveryData(_wallet, _newOwners, _newThreshold, _nonce));
address lastSigner = address(0);
for (uint256 i = 0; i < _signatures.length; i++) {
SignatureData memory value = _signatures[i];
if (value.signature.length == 0){
require(isGuardian(_wallet, msg.sender), "SM: sender not a guardian");
require(msg.sender == value.signer, "SM: null signature should have the signer as the sender");
}else{
validateGuardianSignature(_wallet, recoveryHash, value.signer, value.signature);
}
require(value.signer > lastSigner, "SM: duplicate signers/invalid ordering");
confirmedHashes[recoveryHash][value.signer] = true;
lastSigner = value.signer;
}
//
if (!_execute) return;
uint256 _approvalCount = getRecoveryApprovals(_wallet, _newOwners, _newThreshold);
require(_approvalCount >= guardiansThreshold, "SM: confirmed signatures less than threshold");
_executeRecovery(_wallet, _newOwners, _newThreshold, _approvalCount);
}

Finalize Recovery

And finally, after the grace period is over, anyone can call finalizeRecovery. If you are building a recovery interface, you can setup a relayer to automatically execute the finalize Recovery to make it a seamless experience for the owner.

/social_recovery/SocialRecoveryModule.sol
/**
* @notice Finalizes an ongoing recovery request if the recovery period is over.
* The method is public and callable by anyone to enable orchestration.
* @param _wallet The target wallet.
*/
function finalizeRecovery(address _wallet) external whenRecovery(_wallet) {
RecoveryRequest storage request = recoveryRequests[_wallet];
require(uint64(block.timestamp) >= request.executeAfter, "SM: recovery period still pending");
address[] memory newOwners = request.newOwners;
uint256 newThreshold = request.newThreshold;
delete recoveryRequests[_wallet];

Safe safe = Safe(payable(_wallet));
address[] memory owners = safe.getOwners();

for (uint256 i = (owners.length - 1); i > 0; --i) {
bool success = safe.execTransactionFromModule({
to: _wallet,
value: 0,
data: abi.encodeCall(OwnerManager.removeOwner, (owners[i - 1], owners[i], 1)),
operation: Enum.Operation.Call
});
if (!success) {
revert("SM: owner removal failed");
}
}

for (uint256 i = 0; i < newOwners.length; i++) {
require(!isGuardian(_wallet, newOwners[i]), "SM: new owner cannot be guardian");
bool success;
if (i == 0){
if (newOwners[i] == owners[i]) continue;
success = safe.execTransactionFromModule({
to: _wallet,
value: 0,
data: abi.encodeCall(OwnerManager.swapOwner, (SENTINEL_OWNERS, owners[i], newOwners[i])),
operation: Enum.Operation.Call
});
if (!success) {
revert("SM: owner replacement failed");
}
continue;
}
success = safe.execTransactionFromModule({
to: _wallet,
value: 0,
data: abi.encodeCall(OwnerManager.addOwnerWithThreshold, (newOwners[i], 1)),
operation: Enum.Operation.Call
});
if (!success) {
revert("SM: owner addition failed");
}
}


if (newThreshold > 1){
bool success = safe.execTransactionFromModule({
to: _wallet,
value: 0,
data: abi.encodeCall(OwnerManager.changeThreshold, (newThreshold)),
operation: Enum.Operation.Call
});
if (!success) {
revert("SM: change threshold failed");
}
}

emit RecoveryFinalized(_wallet, newOwners, newThreshold, walletsNonces[_wallet] - 1);
}

Cancel Recovery

At any time during a recovery process, the owner can call cancelRecovery and remove their guardians form the Account.

/social_recovery/SocialRecoveryModule.sol
/**
* @notice Lets the owner cancel an ongoing recovery request.
* @param _wallet The target wallet.
*/
function cancelRecovery(address _wallet) external authorized(_wallet) whenRecovery(_wallet) {
delete recoveryRequests[_wallet];
emit RecoveryCanceled(_wallet, walletsNonces[_wallet] - 1);
}

The Account Recovery Module is open-source and can be found on github

· 2 min read

candide-contracts-4337

Candide Wallet released its second version of its core contract on testnet, addressing key issues from its previous version. The new contract is simple and doesn't require complex fallbacks or delegate calls like the previous one. Only four methods are added to Safe Contracts, allowing it to be ERC-4337 compatible.

Candide Wallet is an EIP-4337 Smart Wallet. This ERC is a specification to add account abstraction functionality to Ethereum and EVM-compatible chains without modifying the consensus rules.

To understand Candide Contracts, it is vital to know the key component that the Global ERC-4337 EntryPoint contract needs from a Smart Account. A Smart Account needs 2 essential methods for the EntryPoint to interact with it: validateUserOp and execute. Candide wallet has 2 more methods for both setup and replacing the EntryPoint for upgradability.

candide-contracts-entrypoint

The Setup method

Candide wallet is a modified version of Safe to accommodate for the entrypoint. The setup method simply calls the setup in Safe with one more argument: the _entryPoint address

setup-method-with-entrypoint

execute-private-method

The validation method

This method blocks unauthorized users to submit operations for the wallet. It's called by the EntryPoint to validate two things: the user's signature and the nonce.

_validateSignature call the signature validation of Safe. It returns zero on success and catches the revert in case the signature is not valid and returns 1 (SIG_VALIDATION_FAILED).

_validateAndUpdateNonce makes sure that the nonce coming from the UserOperation is valid and increments the nonce for the Account.

The missingAccountFunds is the amount that the Account needs to deposit in the EntryPoint to execute the transaction. It is zero in the case a paymaster is sponsoring, or if the Account already has a previous sufficient deposit to cover the gas cost associated.

validate-with-entrypoint

The execution method

This function allows the Entrypoint to execute a transaction without any further confirmations. It has to check one condition: Make sure the transaction is indeed coming from Entrypoint.

execute-from-entrypoint

Replace EntryPoint

Lastly, if a new EntryPoint is introduced, whether to add new functionality, improve gas efficiency, or fix a critical security bug, users can self-call to replace their account’s code address with a new code address containing code that points to a new EntryPoint

replace-entrypoint

Those are essentially the four main methods used for Candide Wallet contracts. Find the CandideWallet.sol on github.