General Information
This assessment, like the MIP21 assessment, also deviates from the standard smart contract technical assessment format because of the idiosyncratic nature of the RWA collateral types. This assessment is a final technical evaluation of MIP22 prior to deployment in the Maker Protocol, and is intended to be a more thorough assessment of all the features. This assessment may be re-used, in part, for other RWA assessments that follow the pattern of MIP22.
For this assessment, we will be focusing on the New Silver NS2DRP token, as well as the infrastructure for all centrifuge tokens.
- Symbol: NS2DRP
-
Address(es):
- NS2DRP: 0xE4C72b4dE5b0F9ACcEA880Ad0b1F944F85A9dAA0
- Relevant MIP information:
- Total supply: 100,044.211751766894967619
- Github repository:
-
Can use existing MCD collateral type adapter?
- This collateral comes with its own authed join and exit functions. These functions are similar to other RWA onboarding that uses this framework.
Technical Information
- Does the contract implement the ERC20 token standards? Yes.
- Decimals: 18.
- Overflow checks: Yes.
-
Mitigation against allowance race-condition: No, but there is a
permit()
interface. - Upgradeable contract patterns: No.
-
Access control or restriction lists: Yes. There is a
memberlist
. -
Non-standard features or behaviors: Yes.
- Above mentioned
memberlist
- addition of the
permit()
approval pattern (minor bug in version)
- Above mentioned
-
Key addresses:
- The
wards
can callmint()
anddepend()
. Wards are- The deployer address, which cannot be called again (dead)
- The
TinlakeRoot
which can calldepend()
and is controlled by a multisig - The
Tranche
which can callmint()
- The
-
Additional notes:
- In addition to the NS2DRP token, this collateral comes with its own sandbox (much like MIP21). This comes with authed
join()
,exit()
,draw()
, andwipe()
calls that allow the owner to call them. We will review these, and other aspects of the architecture below.
- In addition to the NS2DRP token, this collateral comes with its own sandbox (much like MIP21). This comes with authed
Reviewing the Architecture
The core RWA architecture consists of the following contracts:
- LibNote
- TinlakeManager
The following are considered out of scope for this assessment, and if needed, are subject to governance approval via an executive vote:
- DssSpell
LibNote contract
This is the same LibNote
we use in the core of DSS.
TinlakeManager contract
Vault Operations
join(uint)
exit(uint)
draw(uint)
wipe(uint)
join(uint)
Join takes a uint
wad
as an argument and can only be called by the owner
. It only functions if the Vault is in a safe
, glad
, and live
state. The manager transfers from the owner
to the manager some wad
, gives itself a gem
balance in the vat
, and calls frob()
to place that gem
into vat
. The join()
in this case combines a traditional join
without the adapter, and a lock()
. This method requires the owner
to toss an approval to the manager so it can transfer gem
.
exit(uint)
Exit takes a uint
wad
as an argument and can only be called by the owner
. It only functions if the Vault is in a safe
, glad
, and live
state. The manager calls frob()
to remove the gem
from the vat
. The gem
balance is removed from the vat
and transferred from the manager to the owner
. The exit()
in this case combines a traditional free()
with an exit()
to the owner
.
draw(uint)
Draw takes a uint
wad
as an argument and can only be called by the owner
. It only functions if the Vault is in a safe
, glad
, and live
state. The manager calculates the rate
adjusted amount of wad
it needs to frob()
from the Vault and then calls DaiJoin.exit()
to send that amount of DAI
to the owner
.
wipe(uint)
Wipe takes a uint
wad
as an argument and can only be called by the owner
. It only functions if the Vault is in a safe
, glad
, and live
state. The manager first transfers DAI from the owner
to itself and calls DaiJoin.join()
to get a vat
DAI
balance. It then calculates the rate
adjusted amount of wad
it needs to repay the Vault. The owner
must toss an approval to allow the manager to transfer DAI
tokens.
Administration
setOwner(address)
migrate(address)
setOwner(address)
This allows the current owner
to reassign the ownership to another contract. Reminder, the owner can call join()
, exit()
, draw()
, wipe()
, and take()
.
migrate(address)
Periodically, changes may be made to the Maker Protocol that require module upgrades. Some of these changes may impact an interface or add a feature that could be required in the manager. For this reason, the smart contract domain team suggested the addition of migrate()
. This function can only be called by a GSM delayed Maker governance to perform the following actions:
- call
vat.hope()
on a newmanager
- give the new
manager
an infiniteDAI
approval - give the new
manager
an infinitegem
approval - and
cage()
the old manager
NOTE: Because this cages the existing manager, it also means the owner could call tell()
and take()
after. For this reason, it’s best to take the following steps in the same block after a call to migrate()
:
- move the vault
- deny the manager on the vat
- move the vault and any erc20 balances to a new manager
Liquidation
tell()
unwind(uint)
sink()
recover()
tell()
Tell kicks off a liquidation. Typically in MCD this would occur via the cat.bite()
or dog.bark()
; however, in MIP22
there are much longer liquidations with a chance to recover DAI. tell()
starts the process. Once started, tell()
liquidates the entire Vault irreversibly changing the state of safe
to false
. This means, if governance triggers a tell, it must be followed to completion.
unwind(uint)
This is the primary liquidation function. Unwind performs the function of a flip.tend()
for the protocol. Anyone can call this function. It will recover DAI
, paying down the debt for the Vault and removing the corresponding gem
amounts.
sink()
Sink is called when the liquidation process has failed, and we must accrue the bad debt to sin
. This function sets glad
to false
, cleans up the Vault, and adds the balance to bad debt in the system. This bad debt will later be resolved with either the surplus buffer if available, or flop()
auctions.
recover(uint)
Recover is a public function similar to unwind()
, but it can only be called after the debt has been written off with sink()
. This allows any possible long tail of recoverable debt to be collected and sent back to the surplus. We do this here because we already accounted for the remaining debt as bad when sink()
was called. This function can be called until the adapter is no longer live()
(the result of calling migrate()
or cage()
).
Global Settlement
take(uint)
cage()
take(uint)
Take only functions once the adapter has been caged with cage()
or the migrate()
call. It allows the owner
to finish up any pending liquidations. A call to tell()
must happen first. Also, see the note in migrate()
about the actions one should take after a migration.
cage()
This function can be called by governance after the GSM delay to disable the manager. Only tell()
and take()
to facilitate a liquidations may be called after a cage()
.
Additional Comments
This does not use a typical Maker oracle. Instead, it uses a DSValue
contract that is periodically updated by governance with the correct price. Since the price of the asset doesn’t change, and is instead related to the amount of debt
taken against the position by the owner and the fees, this collateral can go a number of weeks without an update. Just like with MIP21, MIP22 doesn’t currently have an oracle attack surface beyond the DSValue
authorization, which is just governance behind the GSM pause.
Contract Risk Summary
There is a medium amount of risk in this contract as the mint()
and memberlist
are under the control of other contracts. The token, however, is always passed between the Maker Protocol, the owner, and the liquidation pool.
On liquidation, MIP22 liquidates the entire Vault. It remains an open question as to how effective this liquidation will be given it’s a longer term process handled by the given pool.
Similar to MIP21 governance must update the price feed for the Vault.
Supporting Materials
Architecture Diagram
Inheritance Diagram
Contracts Description Table
Contract | Type | Bases | ||
---|---|---|---|---|
└ | Function Name | Visibility | Mutability | Modifiers |
ERC20 | Implementation | |||
└ | rely | Public ![]() |
![]() |
auth |
└ | deny | Public ![]() |
![]() |
auth |
└ | add | Internal ![]() |
||
└ | sub | Internal ![]() |
||
└ | Public ![]() |
![]() |
NO❗️ | |
└ | transfer | External ![]() |
![]() |
NO❗️ |
└ | transferFrom | Public ![]() |
![]() |
NO❗️ |
└ | mint | External ![]() |
![]() |
auth |
└ | burn | External ![]() |
![]() |
NO❗️ |
└ | approve | External ![]() |
![]() |
NO❗️ |
└ | push | External ![]() |
![]() |
NO❗️ |
└ | pull | External ![]() |
![]() |
NO❗️ |
└ | move | External ![]() |
![]() |
NO❗️ |
└ | permit | External ![]() |
![]() |
NO❗️ |
MemberlistLike_2 | Implementation | |||
└ | hasMember | Public ![]() |
NO❗️ | |
└ | member | Public ![]() |
![]() |
NO❗️ |
RestrictedToken | Implementation | ERC20 | ||
└ | hasMember | Public ![]() |
NO❗️ | |
└ | Public ![]() |
![]() |
ERC20 | |
└ | depend | Public ![]() |
![]() |
auth |
└ | transferFrom | Public ![]() |
![]() |
checkMember |
Legend
Symbol | Meaning |
---|---|
![]() |
Function can modify state |
![]() |
Function is payable |