The Dharma Smart Wallet has a lot to offer, even for crypto natives
11 min read
Published on November 15, 2019
The emblem and quasi-official mascot of DeFi is the Unicorn, and with good reason — many DeFi adherents insist that participating in the decentralized finance movement in a manner that is both empowering and pleasant is, well, make-believe. As a result, they abandon all hope for the fruitful union of these apparently contradictory and unrealistic ideals, and end up either accepting a sub-par experience or sacrificing on the self-agency and security that makes magical internet money so cool in the first place.
At Dharma, we hold the preposterous belief that, not only does this mythical beast exist, but that we can capture it and tame it. Admittedly, unicorns are proud creatures, and a delicate dance must be performed in order to get close enough to hitch a ride. But the end result — the Dharma Smart Wallet — achieves that mythical combination that lets its owners, novice and veteran alike, have their interest-bearing cake and eat it too.
But you’re no novice. You’ve been around long enough to develop a well-founded skepticism for hyperbolic claims with no substance. This article is for you — let’s get started.
To kick things off, allow me to provide a quick overview of what the Dharma Smart Wallet does and how it works. It’s a meta-transaction enabled multisig that automatically forwards any Dai and USDC you throw at it into Compound. Withdrawals require approval from two sources: one from the user’s Dharma Key Ring, and one from a key on the Dharma Key Registry for added protection against that sketchy hacker (or maybe you are that sketchy hacker—I’m not here to judge).
The Dharma Key Ring is a multisig in its own right, controlled entirely by each user, that stores a unique signing key for each approved device. However, it isn’t meant to be used for actually making transactions, apart from being able to add new keys using existing ones — instead, it exposes the ERC-1271 interface and validates signatures passed to it by the smart wallet before giving the thumbs up to allow the action in question or not.
Speaking of signatures—the Dharma Smart Wallet is up to the hilt in meta-transactions. This totally decouples signing from transaction submission, and even lets us consider using signature schemes other than
secp256k1. As an example, elliptic curves supported by the WebCrypto API could be used to create signing keys that cannot be extracted by the browser!
Of course, there’s a lot of nuance involved in doing meta-transactions securely. Replay protection is really important to get right, and requires that a nonce be incremented every time a valid set of signatures is processed. This requirement is met by performing a recursive call from the smart wallet back into itself as soon as it’s time to get down to business, then catching and handling any reverts that occur downstream.
Let’s walk through a few of the core “flows” of the Dharma Smart Wallet. First, here’s how it handles new wallet deployments, which are intrinsically tied to the first stablecoin deposit:
1. A counterfactual deployment address is derived using the initial user signing key for the smart wallet (i.e. the address of the user’s key ring) as the primary input and is provided to new users as their deposit address.
2. We begin listening on that address, waiting for a Dai or USDC transfer to it, and trigger the deployment of the smart wallet as soon as the tokens land.
3. During contract creation, the constructor first stores the user’s initial signing address (which is actually the counterfactual deployment address of their eventual key ring contract) and sets all necessary approvals for cDai and cUSDC to transfer tokens on their behalf in order to mint cTokens.
4. Then, the smart wallet determines if it has a Dai or USDC token balance, and if so, it will attempt to mint the respective cToken, supplying the full available balance. Any errors are parsed and used to populate an ExternalError event on the off chance they occur.
5. We then continue to listen for subsequent deposits, and trigger additional mints any time that new funds are transferred in.
Now that we’re all warmed up, let’s walk through a more complex example, the first withdrawal:
1. Before initiating the withdrawal, we first determine whether the user’s key ring has been deployed yet — if not, we deploy it to the counterfactual address using the initial signing key on the key ring as the primary input and simultaneously call into the smart wallet to kick off the withdrawal.
2. Once the smart wallet itself is called, it begins by optionally ensuring that enough gas has been supplied (to thwart any of you prospective griefers lurking out there).
3. Next, the smart wallet constructs a message hash using the address of the smart wallet, the implementation version, the address of the key ring, the address of Dharma’s signing key (retrieved from the Dharma Key Registry), the current nonce of the smart wallet, the required gas, the action type, and the parameters of the call.
4. The smart wallet then prefixes and hashes this message hash according to ERC-191 0x45 (e.g. eth_personalSign) and provides this message hash, context for the hash, and the user signature (or signatures!) to the ERC-1271 interface on the key ring by calling its isValidSignature view function.
5. The key ring then parses the signatures, performs ecrecover against the supplied hash for each one, and only returns the ERC-1271 magic return value if each signature resolves to a unique key on the key ring with the appropriate permissions.
6. Once the user signature has been approved, the smart wallet performs ecrecover against the “secure backstop” key as well.
7. After all of this is complete, the smart wallet increments the nonce, makes some sanity checks on the arguments, and immediately performs a recursive call back into a protected “external” function on itself to prevent the call from reverting from that point onward.
8. From within the new call context, the smart wallet ensures that it has been called from the right location and attempts to redeem the specified quantity of the underlying via redeemUnderlying on the given cToken (or will detect the total cToken balance and supply it to redeem if the user has elected to withdraw their entire stack).
9. If the cToken redemption was successful, it proceeds in an attempt to make the transfer — otherwise, it emits an ExternalError event and returns early. Then, if the token transfer fails, the preceding redemption is rolled back by causing the recursive call to revert, and an ExternalError is emitted to signal that the transfer failed.
10. Future withdrawals can then skip the counterfactual key ring deployment step and call into the smart wallet directly.
And for the cool-down, let’s walk through a comparatively simple, yet highly consequential feature — the process by which you can add new keys to your Dharma Key Ring in order to control your smart wallet from multiple distinct devices:
1. In a similar to fashion to the smart wallet, begin by constructing a message hash using the address of the key ring, the implementation version, the current nonce of the key ring, and the parameters of the call.
2. Use a supplied signature to perform ecrecover against that hash and ensure that the revered address maps to an existing key on the key ring with appropriate permissions.
3. Add the new key, emit a KeyModified event, and increment the nonce on the key ring.
4. Now the new key can be used to make withdrawals on the smart wallet, or to manage other keys on the key ring.
Having all of the above functionality, wrapped up in a buttery-smooth, gradient-laden interface to boot, is undeniably gratifying. Still, there’s a prevailing mentality that helper functions and meta-transactions are primarily intended to lend a hand when users don’t know how to handle it themselves. As a result, I’ve been hearing a lot of opinions along the line of:
Yeah, I’ll definitely recommend Dharma to all my normies, but I’m an expert and so will obviously just keep using my trusty old wallet and node to interact with Compound and other DeFi protocols directly, thank you very much.
First off, if you’re actually running your own node, I applaud your steely-eyed principles. That being said, if you’re really such a pro, how come your aunt Mildred (who still thinks Dai is an Eastern philosophy) is now cleaning your clock when it comes to quickly earning interest on new deposits?
Here’s what I mean — by sticking to an EOA, or even a generic multisig or whatever, you are incurring an opportunity cost whenever you’d like to receive stablecoins and convert them into their interest-bearing equivalent. Any time they land at your account (maybe you’ve got a recurring deposit going, or you’re even taking a salary in them), you first need to collect your keys, get in front of a computer, and whirl out whatever Rube Goldberg machine you’ve set up to keep your funds safe before minting some cTokens or what have you.
Meanwhile, Mrs. Millie’s smart wallet has a few highly-specific code paths baked in that don’t require her to have her butt in a seat in order to instantly start earning interest whenever she gets paid — Dharma (or anyone else, for that matter) can simply watch for funds to land and call
repayAndDeposit() on her smart wallet as soon as they do. And yes, you read that function name right — the Dharma Smart Wallet doesn’t currently support borrowing, but these two kinds of functionality actually fit together quite nicely (and it never hurts to plan ahead a touch).
And how is this not a vector for griefing attacks, you might ask? Can’t an attacker gum up the gears when you want to withdraw your stablecoins by re-depositing them?
Well, time to highlight another kick-ass benefit of using a smart wallet — they can process multiple atomic actions, like redeeming cTokens and immediately transferring the underlying tokens to a designated recipient, in one shot. The only grief here is that which your soul must bear, as you futz about with a second transaction and waste precious time and gas. Meanwhile, Mildred has already made her withdrawal and moved on to more complicated things like trying to include an attachment on an email.
Taking this concept a step further, it’s not just about fewer transactions. Performing actions atomically can make all the difference, especially when money’s on the line. You don’t want to expose yourself to unnecessary carry risk, and may not even be able to properly undo some of the first steps in a batch if each action is its own independent transaction. Instead, you want to try each action in sequence, and to stop if you hit a snag and roll back the whole shebang.
The Dharma Smart Wallet has a pretty sweet function for performing these kinds of operations called
executeActionWithAtomicBatchCalls — it will undo all the calls performed prior to a snafu, but still gives you back all of the return data from each of them so you can figure out what went wrong and react appropriately.
By now, you may be starting to get the point:
Ok, I’ll concede that using a smart wallet as my internet piggy-bank makes a lot of sense… making certain operations more autonomous, batching actions, even the meta-transaction stuff all sounds pretty nice. But why should I use Dharma’s smart wallet, then? I’ll just do it myself and deploy my own contracts!
That’s absolutely an option and would confer an instant improvement over the more manual alternative. But I’m willing to go out on a limb and say that you’re not going to deploy and manage your own smart wallet, and here’s why — DeFi is constantly evolving, meaning that you’re going to need to upgrade or migrate this smart wallet of yours pretty frequently, lest it fall into obsolescence.
Even more problematic, there’s the very real possibility that you make some small error along the way and end up getting hacked or permanently bricking your fancy home-grown wallet. Before you go gun-slinging in the Wild West of smart contract land, know that letting the target slip out of your cross-hairs for just an instant often leads to shooting yourself squarely in the foot.
That’s where Dharma comes in. We take on the never-ending task of staying up to date on the latest and greatest in DeFi. We (together with Trail of Bits) are continually reviewing, optimizing, hardening, and improving our underlying smart contracts and supporting infrastructure. And to facilitate this flexible approach, we’ve got one of the most innovative (and forgiving) upgradeability mechanisms out there.
Here’s the standard approach to smart contract upgradeability: deploy a proxy contract that lets a particular, authorized caller update a pointer to an implementation contract in unstructured storage. From there, run-of-the-mill calls to the proxy are delegated to that implementation, which then dictates what action the proxy should take.
This approach works well when you have a specific contract that needs to be upgradeable, as long as you take care not to let the implementation clobber your upgrade logic or muck with the associated storage slots somehow. This is actually more problematic than it sounds, because it makes it potentially impossible to roll back from even one malicious or particularly knuckle-headed upgrade. Furthermore, this family of upgradeability can also add appreciable overhead.
This applies to calls into the proxy itself — for instance, OpenZeppelin transparent proxies will no longer be able to accept payments using the default Solidity
transfer method once the Istanbul hard fork quadruples the cost of storage reads (an
SLOAD for retrieving both the admin account and the implementation account, plus the
DELEGATECALL to the implementation, eats up the entire
2*800 + 700 = 2300 gas stipend).
However, the overhead of paying a bit of extra gas on each call pales in comparison to the added cost, risk, and pain-in-the-ass quotient of performing upgrades on a large group of contracts. It’s not like there’s one Dharma Smart Wallet contract that everybody shares — that would be downright uncouth. Yet, making a call into each and every smart wallet to update the implementation storage slot therein (or expecting each and every user to do the same) is an equivalently vulgar premise.
Our solution is to utilize an “Upgrade Beacon” contract. This contract designates a controller that can update storage slot zero. For any other caller, it simply returns the address held in said slot. Then, each proxy is deployed with the Upgrade Beacon hard-coded into its runtime code, and performs a
STATICCALL to it on deployment, and at the start of every subsequent call it receives, in order to retrieve the implementation that it will
This comes with a veritable smorgasbord of benefits:
SLOAD, and one
DELEGATECALL), or up to 200 less gas if you’re willing to get weird and use runtime storage on a pair of metamorphic contracts (one
EXTCODESIZEto figure out which of the pair is currently set, one
EXTCODECOPYto get the implementation address, and the
At this point, you can tell that there’s a pretty strong case to be made for letting Dharma take care of keeping your smart wallet up to snuff. This, coupled with our built-in “security backstop” to verify ownership changes and withdrawals, does a bang-up job of protecting you from the bad guys. But… what if we are the bad guys? One ruinous upgrade and all your precious coins are ours!
The answer to this conundrum is that we utilize timelocks for such delicate and impactful operations. This applies to both contract upgrades and to account recovery, which is also muy importante. In the unlikely event that you don’t agree with our decision to initiate either process, you have a seven-day window to opt out by withdrawing your funds.
I must mention an important caveat to the above: the upgrade timelock doesn’t apply to rollbacks, since the introduction of a new vulnerability may need to be expeditiously stamped out, or to upgrades that activate a predetermined “contingency” implementation — a minimal smart wallet that gives you sole, complete control over your funds until 48 hours have elapsed, at which point we can push out a patched implementation.
That’s great, but there’s a final wrinkle here: our security backstop is designed so that we need to approve all withdrawals before they are processed. But what if we refuse to sign any of your withdrawal requests during the seven-day window?
To address this possibility and fully close the loop, we will soon be rolling out the Dharma Escape Hatch. This opt-in feature lets you give an account of your choosing “sudo” status, where it can call into your smart wallet at any time and sweep the entire balance. That way, you can un-mount the unicorn if it starts getting unruly, retreating to the safety and comfort of a trusty old steed.