Skip to content

Implementation and testing for core Dharma Token (dToken) contracts, including Dharma Dai and Dharma USD Coin.

License

Notifications You must be signed in to change notification settings

dharma-eng/dharma-token

Repository files navigation

HeaderImg

Dharma Token (dharma-token)

Implementation and testing for core Dharma Token (dToken) contracts, including Dharma Dai and Dharma USD Coin.

Dharma Dai Version Dharma USD Coin Version License Dharma Token CI Coverage Community

Summary

A Dharma Token (or dToken) is an upgradeable ERC20 token with support for meta-transactions that earns interest with respect to a given stablecoin, and is backed by that stablecoin's respective Compound cToken. Interacting with dTokens using the underlying stablecoin is similar to interacting with cTokens, sans borrowing mechanics. In addition, dTokens can be minted and redeemed using the backing cTokens directly.

Interest on dTokens can be accrued at any point, but is automatically accrued whenever new tokens are minted or redeemed, when transfers denominated in underlying tokens are performed, or when the surplus (or excess backing cTokens) is pulled. On accrual, the new exchange rate of the backing cToken is calculated and the dToken exchange rate increases by 9/10ths of the amount of that of the cToken - in other words, the exchange rate of a dToken appreciates at 90% the rate of that of its backing cToken.

Two Dharma Tokens are currently deployed to mainnet: Dharma Dai (dDai) and Dharma USD Coin (dUSDC).

These contracts were reviewed by Trail of Bits for four days in January 2020, including a general security review, a deeper review of internal math and accounting, and a review of meta-transaction functionality. Their findings and recommendations were immediately incorporated into the code, and Manticore test cases were developed and are included in this repository. No audit report is currently available.

Table of Contents

Contract Deployment Addresses and Verified Source Code

Dharma Dai

Dharma USD Coin

Overview

Interaction with Dharma Dai and Dharma USD Coin will mostly be mediated by the Dharma Smart Wallet. To interact with either one directly, use the following ABI (Dharma Token V1) along with the address of the respective token:

[{"constant":true,"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"dTokenName","type":"string"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getVersion","outputs":[{"internalType":"uint256","name":"version","type":"uint256"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"underlyingToReceive","type":"uint256"}],"name":"redeemUnderlyingToCToken","outputs":[{"internalType":"uint256","name":"dTokensBurned","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"dTokenTotalSupply","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"pullSurplus","outputs":[{"internalType":"uint256","name":"cTokenSurplus","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getSurplus","outputs":[{"internalType":"uint256","name":"cTokenSurplus","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"dTokensToBurn","type":"uint256"}],"name":"redeemToCToken","outputs":[{"internalType":"uint256","name":"cTokensReceived","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bool","name":"increase","type":"bool"},{"internalType":"uint256","name":"expiration","type":"uint256"},{"internalType":"bytes32","name":"salt","type":"bytes32"},{"internalType":"bytes","name":"signatures","type":"bytes"}],"name":"modifyAllowanceViaMetaTransaction","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"dTokenDecimals","type":"uint8"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"underlyingEquivalentAmount","type":"uint256"}],"name":"transferUnderlying","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"addedValue","type":"uint256"}],"name":"increaseAllowance","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOfUnderlying","outputs":[{"internalType":"uint256","name":"underlyingBalance","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getSpreadPerBlock","outputs":[{"internalType":"uint256","name":"rateSpread","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getSurplusUnderlying","outputs":[{"internalType":"uint256","name":"underlyingSurplus","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"accrualBlockNumber","outputs":[{"internalType":"uint256","name":"blockNumber","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes4","name":"functionSelector","type":"bytes4"},{"internalType":"bytes","name":"arguments","type":"bytes"},{"internalType":"uint256","name":"expiration","type":"uint256"},{"internalType":"bytes32","name":"salt","type":"bytes32"}],"name":"getMetaTransactionMessageHash","outputs":[{"internalType":"bytes32","name":"messageHash","type":"bytes32"},{"internalType":"bool","name":"valid","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"dTokens","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"underlyingEquivalentAmount","type":"uint256"}],"name":"transferUnderlyingFrom","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"underlyingToReceive","type":"uint256"}],"name":"redeemUnderlying","outputs":[{"internalType":"uint256","name":"dTokensBurned","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"dTokenSymbol","type":"string"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":true,"inputs":[],"name":"getUnderlying","outputs":[{"internalType":"address","name":"underlying","type":"address"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"underlyingToSupply","type":"uint256"}],"name":"mint","outputs":[{"internalType":"uint256","name":"dTokensMinted","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"subtractedValue","type":"uint256"}],"name":"decreaseAllowance","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"accrueInterest","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"supplyRatePerBlock","outputs":[{"internalType":"uint256","name":"dTokenInterestRate","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"exchangeRateCurrent","outputs":[{"internalType":"uint256","name":"dTokenExchangeRate","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"cTokensToSupply","type":"uint256"}],"name":"mintViaCToken","outputs":[{"internalType":"uint256","name":"dTokensMinted","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"dTokensToBurn","type":"uint256"}],"name":"redeem","outputs":[{"internalType":"uint256","name":"underlyingReceived","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"dTokenAllowance","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getCToken","outputs":[{"internalType":"address","name":"cToken","type":"address"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":true,"inputs":[],"name":"totalSupplyUnderlying","outputs":[{"internalType":"uint256","name":"dTokenTotalSupplyInUnderlying","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"minter","type":"address"},{"indexed":false,"internalType":"uint256","name":"mintAmount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"mintDTokens","type":"uint256"}],"name":"Mint","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"redeemer","type":"address"},{"indexed":false,"internalType":"uint256","name":"redeemAmount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"redeemDTokens","type":"uint256"}],"name":"Redeem","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"dTokenExchangeRate","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"cTokenExchangeRate","type":"uint256"}],"name":"Accrue","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"surplusAmount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"surplusCTokens","type":"uint256"}],"name":"CollectSurplus","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"}]
  • Dharma Dai: 0x00000000001876eB1444c986fD502e618c587430
  • Dharma USD Coin: 0x00000000008943c65cAf789FFFCF953bE156f6f8

The complete dToken interface, including ERC20 methods, is as follows:

interface DTokenInterface {
  // Events bear similarity to Compound's supply-related events.
  event Mint(address minter, uint256 mintAmount, uint256 mintDTokens);
  event Redeem(address redeemer, uint256 redeemAmount, uint256 redeemDTokens);
  event Accrue(uint256 dTokenExchangeRate, uint256 cTokenExchangeRate);
  event CollectSurplus(uint256 surplusAmount, uint256 surplusCTokens);

  // These external functions trigger accrual on the dToken and backing cToken.
  function mint(uint256 underlyingToSupply) external returns (uint256 dTokensMinted);
  function redeem(uint256 dTokensToBurn) external returns (uint256 underlyingReceived);
  function redeemUnderlying(uint256 underlyingToReceive) external returns (uint256 dTokensBurned);
  function pullSurplus() external returns (uint256 cTokenSurplus);

  // These external functions only trigger accrual on the dToken.
  function mintViaCToken(uint256 cTokensToSupply) external returns (uint256 dTokensMinted);
  function redeemToCToken(uint256 dTokensToBurn) external returns (uint256 cTokensReceived);
  function redeemUnderlyingToCToken(uint256 underlyingToReceive) external returns (uint256 dTokensBurned);
  function accrueInterest() external;
  function transferUnderlying(address recipient, uint256 underlyingEquivalentAmount) external returns (bool success);
  function transferUnderlyingFrom(address sender, address recipient, uint256 underlyingEquivalentAmount) external returns (bool success);

  // This function provides basic meta-tx support and does not trigger accrual.
  function modifyAllowanceViaMetaTransaction(
    address owner,
    address spender,
    uint256 value,
    bool increase,
    uint256 expiration,
    bytes32 salt,
    bytes calldata signatures
  ) external returns (bool success);

  // View and pure functions do not trigger accrual on the dToken or the cToken.
  function getMetaTransactionMessageHash(
    bytes4 functionSelector, bytes calldata arguments, uint256 expiration, bytes32 salt
  ) external view returns (bytes32 digest, bool valid);
  function totalSupplyUnderlying() external view returns (uint256);
  function balanceOfUnderlying(address account) external view returns (uint256 underlyingBalance);
  function exchangeRateCurrent() external view returns (uint256 dTokenExchangeRate);
  function supplyRatePerBlock() external view returns (uint256 dTokenInterestRate);
  function accrualBlockNumber() external view returns (uint256 blockNumber);
  function getSurplus() external view returns (uint256 cTokenSurplus);
  function getSurplusUnderlying() external view returns (uint256 underlyingSurplus);
  function getSpreadPerBlock() external view returns (uint256 rateSpread);
  function getVersion() external pure returns (uint256 version);
  function getCToken() external pure returns (address cToken);
  function getUnderlying() external pure returns (address underlying);

  // ERC20 events and methods (these do not trigger accrual).
  event Transfer(address indexed from, address indexed to, uint256 value);
  event Approval(address indexed owner, address indexed spender, uint256 value);

  function transfer(address recipient, uint256 amount) external returns (bool);
  function approve(address spender, uint256 amount) external returns (bool);
  function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
  function increaseAllowance(address spender, uint256 addedValue) external returns (bool success);
  function decreaseAllowance(address spender, uint256 subtractedValue) external returns (bool success);

  function totalSupply() external view returns (uint256);
  function balanceOf(address account) external view returns (uint256);
  function allowance(address owner, address spender) external view returns (uint256);
}

Minting

There are two methods to mint new dTokens:

  • mint(uint256 underlyingToSupply) will transfer the specified amount of underlying from the caller to the dToken, which requires that sufficient allowance first be set by calling approve on the underlying and supplying the dToken as the spender, or by using permit if applicable. The underlying will be used to mint the backing cTokens, and dTokens will be given to the caller in proportion to the current exchange rate.
  • mintViaCToken(uint256 cTokensToSupply) will transfer the specified amount of cTokens from the caller to the dToken, which requires that sufficient allowance first be set by calling approve on the cToken and supplying the dToken as the spender. The cTokens will be retained as backing collateral, and dTokens will be given to the caller in proportion to the current exchange rate.

Whenever calling mint, interest accrual will be performed on both the cToken and the dToken - this operation adds quite a bit of additional overhead (and even more so on Dharma Dai, since cDai itself interacts with the Dai Savings Rate contract family). In contrast, calling mintViaCToken only accrues interest on the dToken, and simply calculates what the cToken exchange rate would be if accrual was to be performed at that time. This, along with the avoidance of needing to mint new cTokens, results in significant gas savings over mint.

Redeeming

There are four methods to redeem existing dTokens:

  • redeem(uint256 dTokensToBurn) will take the specified amount of dTokens from the caller, then transfer underlying to them in proportion to the current exchange rate.
  • redeemUnderlying(uint256 underlyingToReceive) is equivalent to redeem, except that the underlying received is passed as the argument instead of the dTokens burned. Note that this method should not be used to "redeem all", since the dTokens will likely appreciate in value between the time the underlying equivalent is supplied and the time the transaction is mined.
  • redeemToCToken(uint256 dTokensToBurn) will take the specified amount of dTokens from the caller, then transfer cTokens to them in proportion to the current exchange rate.
  • redeemUnderlyingToCToken(uint256 underlyingToReceive) is equivalent to redeemToCToken, except that the underlying received is passed as the argument instead of the dTokens burned. Same caveat applies as in redeemUnderlying.

Interest accrual is performed on the both the cToken and the dToken when calling redeem or redeemUnderlying, but only on the dToken when calling redeemToCToken or redeemUnderlyingToCToken. In general, the direct dToken arguments are also slightly more efficient, both in gas usage and in avoidance of rounding errors when redeeming very small amounts.

Transferring

There are a handful of different approaches to transferring dTokens:

In addition to the standard ERC20 approve (which is susceptible to a well-known race condition), allowance can be modified via increaseAllowance, decreaseAllowance, and modifyAllowanceViaMetaTransaction.

Meta-transactions

In order to provide basic meta-transaction support, dToken allowances can be set by providing signatures that are then supplied by arbitrary callers as part of calls to modifyAllowanceViaMetaTransaction(address owner, address spender, uint256 value, bool increase, uint256 expiration, bytes32 salt, bytes calldata signatures), either to increase allowance (when increase = true) or to decrease allowance (when increase = false) The getMetaTransactionMessageHash(bytes4 functionSelector, bytes calldata arguments, uint256 expiration, bytes32 salt) view function can be used to get the message hash that needs to be signed (as a "personal message") in order to generate the signature, with function selector 0x2d657fa5 and arguments abi.encode(owner, spender, value, increase) for modifyAllowanceViaMetaTransaction.

These meta-transactions are unordered, meaning that they are based on a unique message hash rather than on an incrementing nonce per account. This hash is generated from the dToken address, the caller, the function called, and the arguments to the function, including an optional expiration and an arbitrary salt value. Once a specific set of arguments has been used, it cannot be used again. (The Dharma Smart Wallet implements meta-transactions using an incrementing nonce, and is used in place of the native dToken meta-transactions when strict transaction ordering is preferred.)

IMPORTANT NOTE: meta-transactions can be front-run by a griefer in an attempt to disrupt conditional logic on the caller that is predicated on success of the call - to protect against this, calling contracts can perform an allowance check against allowance or a message hash validity check against getMetaTransactionMessageHash prior to performing the call, or can catch reverts originating from the call and perform either of these two checks on failure.

They also utilize ERC-1271 in cases where the owner is a contract address - this means that the dToken will call into a isValidSignature(bytes calldata data, bytes calldata signatures) view function on the contract at the owner account, and that contract will then determine whether or not to allow the meta-transaction to proceed or not. The data parameter is comprised of a 32-byte hash digest, followed by a "context" bytes array that contains the arguments used to generate the hash digest (to be precise, the context is hashed to generate the "message hash", then that message hash is prefixed according to EIP-191 0x45, i.e. geth's personal_sign, and hashed again to generate the hash digest). In cases where the owner is not a contract address (i.e. there is no runtime code at the account), ecrecover will be used instead.

IMPORTANT NOTE: dTokens can be stolen from contracts that implement ERC-1271 in an insecure fashion - do not return the ERC-1271 magic value from an isValidSignature call on your contract unless you're sure that you've properly implemented your desired signature validation scheme!

View functions

Dharma Tokens have a whole host of view functions and pure functions - many are direct analogues of the equivalents on Compound (though they are all actually view functions) and are mostly self-explanatory. That being said, it is important to note that exchangeRateCurrent, supplyRatePerBlock, and getSpreadPerBlock all return values that have been "scaled up" by 10^18, meaning the returned values should be divided by that scaling factor in order to derive the actual value.

Install

To install locally, you'll need Node.js 10 through 12 and Yarn (or npm). To get everything set up:

$ git clone https://github.com/dharma-eng/dharma-token.git
$ cd dharma-token
$ yarn install
$ yarn build

Usage

Tests are performed against a fork of the latest block on mainnet. To run, start the testRPC, trigger the tests, run the linter, and tear down the testRPC (you can do all of this at once via yarn all if you prefer):

$ yarn start
$ yarn test
$ yarn lint
$ yarn stop

You can also run code coverage if you like:

$ yarn build
$ yarn coverage

To run Manticore tests, follow the installation instructions (note that Manticore is only officially supported on Linux) and run:

$ yarn manticoreTest

Notable Transactions

Additional Information

This repository is maintained by @0age and @carlosflrs.

Have any questions or feedback? Join the conversation in the Dharma_HQ Discord server.