The world of decentralized finance (DeFi) is constantly evolving, and MakerDAO stands at the forefront of this innovation. One of its key components, the Peg Stability Module (PSM), has been instrumental in maintaining the stability of Dai, MakerDAO's native stablecoin.
This article dives into the intricacies of the PSM and introduces its newest iteration – the LitePSM – built by Dewiz for enhanced efficiency.
The first Peg Stability Module was deployed in the aftermath of the DeFi Summer back in 2020. At the time, the demand for Dai skyrocketed due to the advent of yield farming, forcing the peg of the stablecoin upwards. The PSM was created by the community with the aim of providing a mean to accommodate the demand, without requiring users to overcollateralize their loans.
As the PSMs grew, so did criticism of it. At one point, the PSMs were backing over 50% of the Dai supply, causing the community to call it out as Dai became more and more dependent on it. Some people in the Maker Community claimed that the overreliance on it would turn Dai into a proxy to centralized stablecoins. Since then, with the introduction of real world assets (RWA) vaults, the size of them has shrunk, backing approximately 11% of the supply of Dai at the time of this writing.
The PSM is a module that facilitates swaps between Dai and other dollar pegged stablecoins. It's the single vault owner for the collateral, due to the use of a GemJoin
that is set up to only authorize the PSM to join the collateral into the system, at an 100% collateralization ratio.
The main goals of the PSM are:
Maintaining the peg: The primary goal of the PSM is to maintain the peg of a stablecoin to its target value. For instance, if a stablecoin is pegged to the US dollar, the PSM works to ensure that the value of the stablecoin remains as close to one US dollar as possible.
Minimizing volatility: By maintaining the peg, the PSM helps minimize the volatility of the stablecoin. This is crucial for a stablecoin to be effective in its role as a stable medium of exchange or a store of value.
Liquidity provision: The PSM often provides liquidity to the stablecoin market, making it easier for users to trade or transact with the stablecoin. This is typically achieved by allowing users to trade the stablecoin with the platform at the same rate to the pegged value.
Confidence building: By ensuring the stability of the stablecoin, the PSM builds confidence among users and investors. This is important for the widespread adoption and use of the stablecoin.
Risk management: The PSM plays a role in managing the systemic risk associated with the stablecoin, especially in scenarios where market conditions are unfavorable or volatile.
Swap facility: The PSM is today widely used by MEV searchers for arbitrage/sandwiching, by intent solvers in systems such as CowSwap and 1inch, as well as by users. It provides no fee, no slippage swaps, facilitating the use of Dai on chain.
The original PSM was developed by the Maker community, and mostly follows the usual "Daiwanese" style of concise names for variables. The architecture is the following:
The main functions are buyGem
and sellGem
, used for swapping Dai and collateral back and forth. Other than the main functions, it only has a couple of others used by governance to set parameters, omitted here for brevity.
// --- Primary Functions ---
function sellGem(address usr, uint256 gemAmt) external {
uint256 gemAmt18 = mul(gemAmt, to18ConversionFactor);
uint256 fee = mul(gemAmt18, tin) / WAD;
uint256 daiAmt = sub(gemAmt18, fee);
gemJoin.join(address(this), gemAmt, msg.sender);
vat.frob(ilk, address(this), address(this), address(this), int256(gemAmt18), int256(gemAmt18));
vat.move(address(this), vow, mul(fee, RAY));
daiJoin.exit(usr, daiAmt);
emit SellGem(usr, gemAmt, fee);
}
function buyGem(address usr, uint256 gemAmt) external {
uint256 gemAmt18 = mul(gemAmt, to18ConversionFactor);
uint256 fee = mul(gemAmt18, tout) / WAD;
uint256 daiAmt = add(gemAmt18, fee);
require(dai.transferFrom(msg.sender, address(this), daiAmt), "DssPsm/failed-transfer");
daiJoin.join(address(this), daiAmt);
vat.frob(ilk, address(this), address(this), address(this), -int256(gemAmt18), -int256(gemAmt18));
gemJoin.exit(usr, gemAmt);
vat.move(address(this), vow, mul(fee, RAY));
emit BuyGem(usr, gemAmt, fee);
}
Swaps in both directions work similarly: the user will issue an ERC-20 approval with the amount of collateral or Dai to be swapped before the call, then call one of the two functions depending on the direction of the swap.
One of the swap functions will then be called, the PSM will adjust the collateral amount to 18 decimals, calculate the fee, transferFrom
the token the user wants to sell, join the tokens in the system (either through AuthGemJoin
or DaiJoin
), call the Vat
(that keeps the vault data for all users), to update the PSM vault (adding collateral/generating debt or the opposite depending on the function called). It will then exit the token the user bought straight to the user, and lastly will transfer fees to the Vow
– the system balance sheet, used for securing the protocol and eventually for surplus/debt auctions.
As seen on the previous section, although very clean, the original PSM was also very inefficient gas usage wise. The gas cost of swapping often negated the benefits of it, namely having no fees and no slippage.
To bring the PSM to nowadays efficiency standards, Dewiz was commissioned by MakerDAO to build a [new version][lite-psm-repo], bringing it in line with today's efficiency standards.
Do not be fooled by the seemingly simple nature of the PSM. There is no such thing as a smart contract module that is easy to build in the OG DeFi protocol, where billions are at stake.
It was definitely a humbling experience. We faced a fair share of problems and we could have not seen it through without the guidance of some senior MakerDAO engineers, who prefer to remain anonymous and will not be mentioned here.
The goal was to make a version that retains ABI retrocompatibility, but is much cheaper to use. The strategy adopted was to make the most efficient implementation possible with plain Solidity (sorry optimizooors, no assembly brackets here), retaining the simplicity, clean code and readability of the original PSM:
LitePSM now holds collateral (gem
) and Dai balances, severely reducing the number of external calls and state changes needed to perform a swap.
The collateral (gem
) can be kept in a different address (pocket
), so yield opportunities can be explored by the protocol.
Vat updates are done by keepers off-band (not to be confused with off-chain).
Swapping fees are also incorporated off-band, and on swaps are only calculated when needed – in the original PSM they were calculated/transferred to Vow
even if fees are set to zero.
Governance is able to grant permissioned swaps without fees to strategic partners.
LitePSM fosters Maker Protocol's operational decentralization, as it reduces the dependency on an arranger with manual actions as in Coinbase Custody, while maintaining the ability to obtain yield on holdings.
LitePSM achieves legal compliance with Coinbase requirements to deliver Marketing Rewards.
The architecture of the new version is the following:
It surely looks more complex, but swaps now are actually simpler:
function sellGem(address usr, uint256 gemAmt) external returns (uint256 daiOutWad) {
uint256 tin_ = tin;
require(tin_ != HALTED, "DssLitePsm/sell-gem-halted");
daiOutWad = _sellGem(usr, gemAmt, tin_);
}
function _sellGem(address usr, uint256 gemAmt, uint256 tin_) internal returns (uint256 daiOutWad) {
daiOutWad = gemAmt * to18ConversionFactor;
uint256 fee;
if (tin_ > 0) {
fee = daiOutWad * tin_ / WAD;
// At this point, `tin_ <= 1 WAD`, so an underflow is not possible.
unchecked {
daiOutWad -= fee;
}
}
gem.transferFrom(msg.sender, pocket, gemAmt);
// This can consume the whole balance including system fees not withdrawn.
dai.transfer(usr, daiOutWad);
emit SellGem(usr, gemAmt, fee);
}
function buyGem(address usr, uint256 gemAmt) external returns (uint256 daiInWad) {
uint256 tout_ = tout;
require(tout_ != HALTED, "DssLitePsm/buy-gem-halted");
daiInWad = _buyGem(usr, gemAmt, tout_);
}
function _buyGem(address usr, uint256 gemAmt, uint256 tout_) internal returns (uint256 daiInWad) {
daiInWad = gemAmt * to18ConversionFactor;
uint256 fee;
if (tout_ > 0) {
fee = daiInWad * tout_ / WAD;
daiInWad += fee;
}
dai.transferFrom(msg.sender, address(this), daiInWad);
gem.transferFrom(pocket, usr, gemAmt);
emit BuyGem(usr, gemAmt, fee);
}
The permissioned no-fee swaps are also quite simple:
modifier toll() {
require(bud[msg.sender] == 1, "DssLitePsm/not-whitelisted");
_;
}
function sellGemNoFee(address usr, uint256 gemAmt) external toll returns (uint256 daiOutWad) {
require(tin != HALTED, "DssLitePsm/sell-gem-halted");
daiOutWad = _sellGem(usr, gemAmt, 0);
}
function buyGemNoFee(address usr, uint256 gemAmt) external toll returns (uint256 daiInWad) {
require(tout != HALTED, "DssLitePsm/buy-gem-halted");
daiInWad = _buyGem(usr, gemAmt, 0);
}
As we can see in the snippets above, the swaps now no longer join the Maker Protocol, nor pay fees to Vow
on every swap. This results in a much cheaper execution, that we expect to take away some serious volume from DEXes, accruing fees for the protocol and ultimately to MKR holders.
Accounting is done off-band by the following functions:
function fill() external returns (uint256 wad) {
wad = rush();
require(wad > 0, "DssLitePsm/nothing-to-fill");
vat.frob(ilk, address(this), address(0), address(this), 0, _int256(wad));
daiJoin.exit(address(this), wad);
emit Fill(wad);
}
function trim() external returns (uint256 wad) {
wad = gush();
require(wad > 0, "DssLitePsm/nothing-to-trim");
daiJoin.join(address(this), wad);
vat.frob(ilk, address(this), address(0), address(this), 0, -_int256(wad));
emit Trim(wad);
}
function chug() external returns (uint256 wad) {
address vow_ = vow;
require(vow_ != address(0), "DssLitePsm/chug-missing-vow");
wad = cut();
require(wad > 0, "DssLitePsm/nothing-to-chug");
daiJoin.join(vow_, wad);
emit Chug(wad);
}
function rush() public view returns (uint256 wad) {
(uint256 Art, uint256 rate,, uint256 line,) = vat.ilks(ilk);
require(rate == RAY, "DssLitePsm/rate-not-RAY");
uint256 tArt = gem.balanceOf(pocket) * to18ConversionFactor + buf;
wad = _min(
_min(
_subcap(tArt, Art),
_subcap(line / RAY, Art)
),
_subcap(vat.Line(), vat.debt()) / RAY
);
}
function gush() public view returns (uint256 wad) {
(uint256 Art, uint256 rate,, uint256 line,) = vat.ilks(ilk);
require(rate == RAY, "DssLitePsm/rate-not-RAY");
uint256 tArt = gem.balanceOf(pocket) * to18ConversionFactor + buf;
wad = _min(
_max(
_subcap(Art, tArt),
_subcap(Art, line / RAY)
),
dai.balanceOf(address(this))
);
}
function cut() public view returns (uint256 wad) {
(, uint256 art) = vat.urns(ilk, address(this));
uint256 cash = dai.balanceOf(address(this));
wad = _min(cash, cash + gem.balanceOf(pocket) * to18ConversionFactor - art);
}
While they are all permissionless, the average user has no incentive to call these functions. For that reason, we also implemented a keeper job to run on the Keeper Network and ensure bookkeeping will be properly carried out.
Here, fill
is used to mint Dai into the PSM, trim
will burn excess Dai, and chug
will send swap fees to the surplus buffer (internal Dai balance of Vow
).
This version also allows governance to set swap fees as in the previous one, along with buf
– the maximum amount of Dai to be kept in the PSM – and also halting swaps completely – a new security feature that can be useful in extreme scenarios.
Through an executive spell, MakerDAO Governance can temporarily halt swaps in LitePSM in any direction or in both at the same time. As in other important modules in the Maker Protocol, there is a new Mom
contract that can bypass the GSM delay and execute the halting as soon as the corresponding spell obtains enough votes.
Check out the repository to see it in detail.
DssLitePsm
relies on pre-minted Dai. It is designed to keep a fixed-sized amount (buf
) of it available most of the time. However, when users call buyGem
, the amount of Dai available will be temporarily larger than buf
.
Scenario A: a user might observe the outstanding amount of Dai and wish to call sellGem
to receive the total of Dai in return. In that scenario, there is a possibility of a transaction calling any of the permissionless bookkeeping functions to front-run them, causing the swap to fail, as the Dai liquidity would be lower than the required amount.
The scenario A above is not possible with the current PSM implementation because each swap is "self-balancing", so no off-band bookkeeping is required.
Scenario B: a large swap might front-run another one, even if unintentionally. Imagine there is 10M
Dai outstanding in DssLitePsm
. If Alice – who wants to swap 8M
– and Bob – who wants to swap 3M
– submit their transactions at the same time, only the first one will be executed.
The scenario B above is not possible with the current PSM implementation because sellGem
is able to mint Dai on-the-fly to fulfill the swap, given that there is enough room in the debt ceiling.
Notice how the same issue happens in buyGem
, however the amount of gem
deposited into DssLitePsm
is only bounded by the debt ceiling, while the amount of Dai
will tend to gravitate towards buf
.
The consequence is that anyone willing to call sellGem
with a value larger than buf
should take care of potential front-running transactions by bundling it with an optional liquidity increase (fill
).
Swaps in DssLitePsm
are generally not subject to slippage. The only exception is when there is a MakerDAO Governance proposal to increase the swapping fees tin
and/or tout
. That is done through an Executive Spell, which is an on-chain smart contract that can be permissionlessly cast (executed) after following the Governance process.
If Alice sends a swap transaction and a spell increasing the fees is cast before her transaction, she will either pay more Dai when buying gems or receive less Dai when selling gems than the originally expected.
This is a highly unlikely scenario, but users or aggregators are able to handle this issue through a wrapper contract.
At this point, an attentive reader might be wondering:
So, by doing all these changes, how much gas is saved on swaps?
We actually got some estimate for you:
The numbers above are generated by Foundry when running forge test --gas-report
. These are the median values obtained from the test suites of the respective repositories. This gives us a ballpark estimation, however the actual gas usage might vary.
The permissioned functions buyGemNoFee
and sellGemNoFee
have a small gas overhead because they need to check whether the caller is authorized to call them or not. That check comes at a cost of 1 extra SLOAD
plus some computation to get the correct slot for the mapping.
This is a relevant change to the Maker Protocol, and as usual security is taken very seriously, being a priority from the start. Security assumptions changed with the change of design: In the previous version the trusted Vat
was in charge of ensuring 100% collateralization, in the new version the PSM itself is responsible to maintain it (governance can still control the size of it by the amount of collateral granted to the PSM vault, as well as its debt ceiling, aka line
).
The code was made in plain solidity for this very reason, as was the 100% coverage test suite. The code was then extensively reviewed by engineers with vast experience with the Maker Protocol, formally verified using Certora and audited by ChainSecurity and SpearBit/CantinaSec.
We are buidlers! Dewiz co-founders are former MakerDAO Core Unit contributors, who have been actively contributing with the Maker Protocol since 2021, and have been engaged in the broader Ethereum ecosystem since 2018.
We enable DeFi innovators to build on top of the main DeFi protocols, providing technical expertise on both on-chain and off-chain integrations that can save thousands of hours in engineering resources and put your project on the fast-track of the new financial world.
Our team has expertise in smart contract engineering for EVM chains, front-end engineering, integrations, supporting services and agile project management.
If you are in need of builders for your project, reach out!
Article written by Oddaf, in collaboration with Amusingaxl.