The Maker Protocol's Rate Accumulation Mechanism
Module Name: Rates Module
Type/Category: Rates
A fundamental feature of the MCD system is to accumulate stability fees on Vault debt balances, as well as interest on Dai Savings Rate (DSR) deposits.
The mechanism used to perform these accumulation functions is subject to an important constraint: accumulation must be a constant-time operation with respect to the number of Vaults and the number of DSR deposits. Otherwise, accumulation events would be very gas-inefficient (and might even exceed block gas limits).
For both stability fees and the DSR, the solution is similar: store and update a global "cumulative rate" value (per-collateral for stability fees), which can then be multiplied by a normalized debt or deposit amount to give the total debt or deposit amount when needed.
This can be described more concretely with mathematical notation:
Discretize time in 1-second intervals, starting from t_0;
Let the (per-second) stability fee at time t have value F_i (this generally takes the form 1+x, where x is small)
Let the initial value of the cumulative rate be denoted by R_0
Let a Vault be created at time t_0 with debt D_0 drawn immediately; the normalized debt A (which the system stores on a per-Vault basis) is calculated as D_0/R_0
Then the cumulative rate R at time T is given by:
And the total debt of the Vault at time t would be:
In the actual system, R is not necessarily updated with every block, and thus actual R values within the system may not have the exact value that they should in theory. The difference in practice, however, should be minor, given a sufficiently large and active ecosystem.
Detailed explanations of the two accumulation mechanisms may be found below.
Stability fee accumulation in MCD is largely an interplay between two contracts: the Vat (the system's central accounting ledger) and the Jug (a specialized module for updating the cumulative rate), with the Vow involved only as the address to which the accumulated fees are credited.
The Vat stores, for each collateral type, an Ilk
struct that contains the cumulative rate (rate
) and the total normalized debt associated with that collateral type (Art
). The Jug stores the per-second rate for each collateral type as a combination of a base
value that applies to all collateral types, and a duty
value per collateral. The per-second rate for a given collateral type is the sum of its particular duty
and the global base
.
Calling Jug.drip(bytes32 ilk)
computes an update to the ilk's rate
based on duty
, base
, and the time since drip
was last called for the given ilk (rho
). Then the Jug invokes Vat.fold(bytes32 ilk, address vow, int rate_change)
which:
adds rate_change
to rate
for the specified ilk
increases the Vow's surplus by Art*rate_change
increases the system's total debt (i.e. issued Dai) by Art*rate_change
.
Each individual Vault (represented by an Urn
struct in the Vat) stores a "normalized debt" parameter called art
. Any time it is needed by the code, the Vault's total debt, including stability fees, can be calculated as art*rate
(where rate
corresponds to that of the appropriate collateral type). Thus an update to Ilk.rate
via Jug.drip(bytes32 ilk)
effectively updates the debt for all Vaults collateralized with ilk
tokens.
Suppose at time 0, a Vault is opened and 20 Dai is drawn from it. Assume that rate
is 1; this implies that the stored art
in the Vault's Urn
is also 20. Let the base
and duty
be set such that after 12 years, art*rate
= 30 (this corresponds to an annual stability of roughly 3.4366%). Equivalently, rate
= 1.5 after 12 years. Assuming that base + duty
does not change, the growth of the effective debt can be graphed as follows:
Now suppose that at 12 years, an additional 10 Dai is drawn. The debt vs time graph would change to look like:
What art
would be stored in the Vat to reflect this change? (hint: not 30!) Recall that art
is defined from the requirement that art * rate
= Vault debt. Since the Vault's debt is known to be 40 and rate
is known to be 1.5, we can solve for art
: 40/1.5 ~ 26.67.
The art
can be thought of as "debt at time 0", or "the amount of Dai that if drawn at time zero would result in the present total debt". The graph below demonstrates this visually; the length of the green bar extending upwards from t = 0 is the post-draw art
value.
Some consequences of the mechanism that are good to keep in mind:
There is no stored history of draws or wipes of Vault debt
There is no stored history of stability fee changes, only the cumulative effective rate
The rate
value for each collateral perpetually increases (unless the fee becomes negative at some point)
drip
?The system relies on market participants to call drip
rather than, say, automatically calling it upon Vault manipulations. The following entities are motivated to call drip
:
Keepers seeking to liquidate Vaults (since the accumulation of stability fees can push a Vault's collateralization ratio into unsafe territory, allowing Keepers to liquidate it and profit in the resulting collateral auction)
Vault owners wishing to draw Dai (if they don't call drip
prior to drawing from their Vault, they will be charged fees on the drawn Dai going back to the last time drip
was called—unless no one calls drip
before they repay their Vault, see below)
MKR holders (they have a vested interest in seeing the system work well, and the collection of surplus in particular is critical to the ebb and flow of MKR in existence)
Despite the variety of incentivized actors, calls to drip
are likely to be intermittent due to gas costs and tragedy of the commons until a certain scale can be achieved. Thus the value of the rate
parameter for a given collateral type may display the following time behavior:
Debt drawn and wiped between rate
updates (i.e. between drip
calls) would have no stability fees assessed on it. Also, depending on the timing of updates to the stability fee, there may be small discrepancies between the actual value of rate
and its ideal value (the value if drip
were called in every block). To demonstrate this, consider the following:
at t = 0, assume the following values:
in a block with t = 28, drip
is called—now:
in a block with t = 56, the fee is updated to a new, different value:
in a block with t = 70, drip
is called again; the actual value of rate
that obtains is:
however, the "ideal" rate
(if drip
were called at the start of every block) would be:
Depending on whether f > g or g > f, the net value of fees accrued will be either too small or too large. It is assumed that drip
calls will be frequent enough such inaccuracies will be minor, at least after an initial growth period. Governance can mitigate this behavior by calling drip
immediately prior to fee changes. The code in fact enforces that drip
must be called prior to a duty
update, but does not enforce a similar restriction for base
(due to the inefficiency of iterating over all collateral types).
DSR accumulation is very similar to stability fee accumulation. It is implemented via the Pot, which interacts with the Vat (and again the Vow's address is used for accounting for the Dai created). The Pot tracks normalized deposits on a per-user basis (pie[usr]
) and maintains a cumulative interest rate parameter (chi
). A drip
function analogous to that of Jug is called intermittently by economic actors to trigger savings accumulation.
The per-second (or "instantaneous") savings rate is stored in the dsr
parameter (analogous to base+duty
in the stability fee case). The chi
parameter as a function of time is thus (in the ideal case of drip
being called every block) given by:
where chi_0 is simply chi(t_0).
Suppose a user joins N Dai into the Pot at time t_0. Then, their internal savings Dai balance is set to:
The total Dai the user can withdraw from the Pot at time t is:
Thus we see that updates to chi
effectively increase all Pot balances at once, without having to iterate over all of them.
After updating chi
, Pot.drip
then calls Vat.suck
with arguments such that the additional Dai created from this savings accumulation is credited to the Pot contract while the Vow's sin
(unbacked debt) is increased by the same amount (the global debt and unbacked debt tallies are increased as well). To accomplish this efficiently, the Pot keeps track of a the total sum of all individual pie[usr]
values in a variable called Pie
.
The following points are useful to keep in mind when reasoning about savings accumulation (all have analogs in the fee accumulation mechanism):
if drip
is called only infrequently, the instantaneously value of chi
may differ from the ideal
the code requires that drip
be called prior to dsr
changes, which eliminates deviations of chi
from its ideal value due to such changes not coinciding with drip
calls
chi
is a monotonically increasing value unless the effective savings rate becomes negative (dsr
< ONE
)
There is no stored record of depositing or withdrawing Dai from the Pot
There is no stored record of changes to the dsr
drip
?The following economic actors are incentivized (or forced) to call Pot.drip
:
any user withdrawing Dai from the Pot (otherwise they lose money!)
any user putting Dai into the Pot—this is not economically rational, but is instead forced by smart contract logic that requires drip
to be called in the same block as new Dai is added to the Pot (otherwise, an economic exploit that drains system surplus is possible)
any actor with a motive to increase the system debt, for example a Keeper hoping to trigger flop (debt) auctions
Let's see how to set a rate value in practice. Suppose it is desired to set the DSR to 0.5% annually. Assume the real rate will track the ideal rate. Then, we need a per-second rate value r such that (denoting the number of seconds in a year by N):
An arbitrary precision calculator can be used to take the N-th root of the right-hand side (with N = 31536000 = 3652460*60), to obtain:
The dsr
parameter in the Pot implementation is interpreted as a ray
, i.e. a 27 decimal digit fixed-point number. Thus we multiply by 10^27 and drop anything after the decimal point:
The dsr
could then be set to 0.5% annually by calling:
Pot.file("dsr", 1000000000158153903837946258)
The Dai Savings Rate
Contract Name: pot.sol
Type/Category: DSS —> Rates Module
The Pot is the core of theDai Savings Rate
. It allows users to deposit dai
and activate the Dai Savings Rate and earning savings on their dai
. The DSR is set by Maker Governance, and will typically be less than the base stability fee to remain sustainable. The purpose of Pot is to offer another incentive for holding Dai.
mul(uint, uint)
, rmul(uint, uint)
, add(uint, uint)
& sub(uint, uint)
- will revert on overflow or underflow
rpow(uint x, uint n, uint base)
, used for exponentiation in drip
, is a fixed-point arithmetic function that raises x
to the power n
. It is implemented in assembly as a repeated squaring algorithm. x
(and the result) are to be interpreted as fixed-point integers with scaling factor base
. For example, if base == 100
, this specifies two decimal digits of precision and the normal decimal value 2.1 would be represented as 210; rpow(210, 2, 100)
should return 441 (the two-decimal digit fixed-point representation of 2.1^2 = 4.41). In the current implementation, 10^27 is passed for base
, making x
and the rpow
result both of type ray
in standard MCD fixed-point terminology. rpow
’s formal invariants include “no overflow” as well as constraints on gas usage.
wards
are allowed to call protected functions (Administration)
pie
- stores the address' Pot
balance.
Pie
- stores the total balance in the Pot
.
dsr
- the dai savings rate
. It starts as 1
(ONE = 10^27
), but can be updated by governance.
chi
- the rate accumulator. This is the always increasing value which decides how much dai
- given when drip()
is called.
vat
- an address that conforms to a VatLike
interface. It is set during the constructor and cannot be changed.
vow
- an address that conforms to a VowLike
interface. Not set in constructor. Must be set by governance.
rho
- the last time that drip is called.
The values of dsr
and vow
can be changed by an authorized address in the contract (i.e. Maker Governance). The values of chi
, pie
, Pie
, and rho
are updated internally in the contract and cannot be changed manually.
Calculates the most recent chi
and pulls dai
from the vow
(by increasing the Vow
's Sin
).
A user should always make sure that this has been called before calling the exit()
function.
drip has to be called before a user join
s and it is in their interest to call it again before they exit
, but there isn't a set rule for how often drip is called.
uint wad
this parameter is based on the amount of dai (since wad
= dai
/ chi
) that you want to join
to the pot
. The wad * chi
must be present in the vat
and owned by the msg.sender
.
the msg.sender
's pie
amount is updated to include the wad
.
the total Pie
amount is also updated to include the wad
.
exit()
essentially functions as the exact opposite of join()
.
uint wad
this parameter is based on the amount of dai that you want to exit
the pot
. The wad * chi
must be present in the vat
and owned by the pot
and must be less than msg.sender
's pie
balance.
The msg.senders
pie
amount is updated by subtracting the wad
.
The total Pie
amount is also updated by subtracting the wad
.
Various file function signatures for administering Pot
:
Setting new dsr (file(bytes32, uint256)
)
Setting new vow (file(bytes32, address)
)
The primary usage will be for addresses
to store their dai
in the pot
to accumulate interest over time
The dsr
is set (globally) through the governance system. It can be set to any number > 0%. This includes the possibility of it being set to a number that would cause the DSR to accumulate faster than the collective Stability Fees, thereby accruing system debt and eventually causing MKR to be minted.
If drip()
has not been called recently before an address calls exit()
they will not get the full amount they have earned over the time of their deposit.
If a user wants to join
or exit
1 DAI into/from the Pot, they should send a wad
= to 1 / chi
as the amount moved from their balance will be 1 * chi
(for an example of this, see DSS-Proxy-Actions
A bug in the Pot
could lead to locking of dai
if the exit()
function or the underlying vat.suck()
or vat.move()
functions were to have bugs.
The dsr
rate initially can be set through the Chief. Governance will be able to change the DSR based on the rules that the DS-Chief employs (which would include a Pause for actions).
One serious risk is if governance chooses to set the dsr
to an extremely high rate, this could cause the system's fees to be far too high. Furthermore, if governance allows the dsr
to (significantly) exceed the system fees, it would cause debt to accrue and increase the Flop auctions.
Accumulation of Stability Fees for Collateral Types
Contract Name: Jug
Type/Category: DSS —> Rates Module
The primary function of the Jug smart contract is to accumulate stability fees for a particular collateral type whenever its drip()
method is called. This effectively updates the accumulated debt for all Vaults of that collateral type as well as the total accumulated debt as tracked by the Vat (global) and the amount of Dai surplus (represented as the amount of Dai owned by the Vow).
Ilk
: contains two uint256
values—duty
, the collateral-specific risk premium, and rho
, the timestamp of the last fee update
VatLike
: mock contract to make Vat interfaces callable from code without an explicit dependency on the Vat contract itself
wards
: mapping(address => uint)
that indicates which addresses may call administrative functions
ilks
: mapping (bytes32 => Ilk)
that stores an Ilk
struct for each collateral type
vat
: a VatLike
that points the the system's Vat contract
vow
: the address
of the Vow contract
base
: a uint256
that specifies a fee applying to all collateral types
These methods require wards[msg.sender] == 1
(i.e. only authorized users may call them).
rely
/deny
: add or remove authorized users (via modifications to the wards
mapping)
init(bytes32)
: start stability fee collection for a particular collateral type
file(bytes32, bytes32, uint)
: set duty
for a particular collateral type
file(bytes32, data)
: set the base
value
file(bytes32, address)
: set the vow
value
drip(bytes32)
: collect stability fees for a given collateral type
drip
drip(bytes32 ilk)
performs stability fee collection for a specific collateral type when it is called (note that it is a public function and may be called by anyone). drip
does essentially three things:
calculates the change in the rate parameter for the collateral type specified by ilk
based on the time elapsed since the last update and the current instantaneous rate (base + duty
);
calls Vat.fold
to update the collateral's rate
, total tracked debt, and Vow surplus;
updates ilks[ilk].rho
to be equal to the current timestamp.
The change in the rate is calculated as:
where "now" represents the current time, "rate" is Vat.ilks[ilk].rate
, "base" is Jug.base
, "rho" is Jug.ilks[ilk].rho
, and "duty" is Jug.ilks[ilk].duty
. The function reverts if any sub-calculation results in under- or overflow. Refer to the Vat documentation for more detail on fold
.
rpow
rpow(uint x, uint n, uint b)
, used for exponentiation in drip
, is a fixed-point arithmetic function that raises x
to the power n
. It is implemented in Solidity assembly as a repeated squaring algorithm. x
and the returned value are to be interpreted as fixed-point integers with scaling factor b
. For example, if b == 100
, this specifies two decimal digits of precision and the normal decimal value 2.1 would be represented as 210; rpow(210, 2, 100)
returns 441 (the two-decimal digit fixed-point representation of 2.1^2 = 4.41). In the current implementation, 10^27 is passed for b
, making x
and the rpow
result both of type ray
in standard MCD fixed-point terminology. rpow
's formal invariants include "no overflow" as well as constraints on gas usage.
Jug stores some sensitive parameters, particularly the base rate and collateral-specific risk premiums that determine the overall stability fee rate for each collateral type. Its built-in authorization mechanisms need to allow only authorized MakerDAO governance contracts/actors to set these values. See "Failure Modes" for a description of what can go wrong if parameters are set to unsafe values.
init(bytes32 ilk)
must called when a new collateral is added (setting duty
via file()
is not sufficient)—otherwise rho
will be uninitialized and fees will accumulate based on a start date of January 1st, 1970 (start of Unix epoch).
base + Ilk.duty
imbalance in drip()
A call to drip(bytes32 ilk)
will add the base
rate to the Ilk.duty
rate. The rate is a calculated compounded rate, so rate(base + duty) != rate(base) + rate(duty)
. This means that if base is set, the duty will need to be set factoring the existing compounding factor in base, otherwise the result will be outside of the rate tolerance. Updates to the base
value will require all of the ilks
to be updated as well.
If drip()
is called very infrequently for some collateral types (due, for example, to low overall system usage or extremely stable collateral types that have essentially zero liquidation risk), then the system will fail to collect fees on Vaults opened and closed between drip()
calls. As the system achieves scale, this becomes less of a concern, as both Keepers and MKR holders are have an incentive to regularly call drip (the former to trigger liquidation auctions, the latter to ensure that surplus accumulates to decrease MKR supply); however, a hypothetical asset with very low volatility yet high risk premium might still see infrequent drip calls at scale (there is not at present a real-world example of this—the most realistic possibility is base
being large, elevating rates for all collateral types).
Various parameters of Jug may be set to values that damage the system. While this can occur by accident, the greatest concern is malicious attacks, especially by an entity that somehow becomes authorized to make calls directly to Jug's administrative methods, bypassing governance. Setting duty
(for at least one ilk) or base
too low can lead to Dai oversupply; setting either one too high can trigger excess liquidations and therefore unjust loss of collateral. Setting a value for vow
other than the true Vow's address can cause surplus to be lost or stolen.