Implementation and testing for core Dharma Token (dToken) contracts, including Dharma Dai and Dharma USD Coin.
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.
- Contract Deployment Addresses and Verified Source Code
- Overview
- Install
- Usage
- Notable Transactions
- Additional Information
- Dharma Dai
- Dharma Dai Upgrade Beacon
- Dharma Dai Upgrade Beacon Controller
- Dharma Dai Initializer Implementation
- Dharma Dai Implementation V1
- Dharma Dai Implementation V0 (emergency fallback implementation that pauses minting and pulling surplus)
- Dharma USD Coin
- Dharma USD Coin Upgrade Beacon
- Dharma USD Coin Upgrade Beacon Controller
- Dharma USD Coin Initializer Implementation
- Dharma USD Coin Implementation V1
- Dharma USD Coin Implementation V0 (emergency fallback implementation that pauses minting and pulling surplus)
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);
}
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 callingapprove
on the underlying and supplying the dToken as the spender, or by usingpermit
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 callingapprove
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
.
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 toredeem
, 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 toredeemToCToken
, except that the underlying received is passed as the argument instead of the dTokens burned. Same caveat applies as inredeemUnderlying
.
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.
There are a handful of different approaches to transferring dTokens:
transfer(address recipient, uint256 amount)
will simply send dTokens from the caller to the recipient.approve(address spender, uint256 amount)
followed bytransferFrom(address sender, address recipient, uint256 amount)
will allow the caller to designate a "sender" which will then be able to send dTokens on their behalf.transferUnderlying(address recipient, uint256 underlyingEquivalentAmount)
is equivalent totransfer
, except that the underlying equivalent value to transfer is passed as the argument, and the amount of dTokens to transfer will be determined using the current exchange rate. Note that the amount of dTokens transferred will be rounded up, meaning that slightly more than the specified underlying equivalent value may be transferred. This function will also accrue interest on the dToken.approve(address spender, uint256 amount)
followed bytransferUnderlyingFrom(address sender, address recipient, uint256 underlyingEquivalentAmount)
is equivalent totransferFrom
, except that the underlying equivalent value to transfer is passed as the argument. This function will also accrue interest on the dToken. Note that the argument toapprove
still needs to be denominated in 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
.
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 againstgetMetaTransactionMessageHash
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!
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.
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
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
- Dharma Dai Deployment
- Dharma USD Coin Deployment
- Dharma Dai upgraded to DharmaDaiInitializer
- Dharma USD Coin upgraded to DharmaUSDCInitializer
- Dharma Dai initialized
- Dharma USD Coin initialized
- Dharma Dai upgraded to DharmaDaiImplementationV1
- Dharma USD Coin upgraded to DharmaUSDCImplementationV1
- First Dharma Dai minted
- First Dharma USD Coin minted
- Ownership of Dharma Dai Upgrade Beacon Controller transferred to Dharma Upgrade Beacon Controller Manager
- Ownership of Dharma USD Coin Upgrade Beacon Controller transferred to Dharma Upgrade Beacon Controller Manager
- Dharma USD Coin upgraded to "hotfix" DharmaUSDCImplementationV1
This repository is maintained by @0age and @carlosflrs.
Have any questions or feedback? Join the conversation in the Dharma_HQ Discord server.