-
Notifications
You must be signed in to change notification settings - Fork 3
Feat: introduce Safebox
contracts
#1
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me - based on our discussions
src/Safebox.sol
Outdated
* @param to The destination address. | ||
* @param amount The amount to be transfered. | ||
*/ | ||
function _safeTransfer(address token, address to, uint256 amount) internal { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd reduce the verbosity of the code just using a simple transfer
. I don't see Maker interacting with a token that doesn't revert
and this contract is behind governance setting it up for a specific purpose. This is only useful for contracts that are open to any token.
At least I'd remove the internal function and just put the code in withdraw
(same for deposit
) as there is not reuse of it. But honestly I'd just keep it simple with the regular transfer
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense, removed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another thing I considered was to make token
a constructor parameter instead of a parameter for withdraw
.
As it is, the Safebox can be seen as multi-token, but there will probably never be a case for it to hold more than 1 asset.
* @param data The new value of the parameter. | ||
*/ | ||
function file(bytes32 what, address data) external auth { | ||
if (what == "recipient") { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be blocked when VAT is not live.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
++
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
src/Safebox.sol
Outdated
|
||
/** | ||
* @notice Approves the change in the recipient. | ||
* @dev Reverts if `previousRecipient` has not been set or if `_recipient` does not match it. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
previousRecipient
=> pendingRecipient
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
src/Safebox.sol
Outdated
* @title A safebox for digital assets. | ||
* @notice | ||
* - The `owner` is in full control of how much and when it can send assets to `recipient`. | ||
* - If MakerDAO governance ever executes an Emergency Shutdown, anyone can send assets to `recipient`. This prevents assets being stuck in this contract when the governance smart contract is no longer operational. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line is really long
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Visual glitch on my editor. Already fixed.
src/Safebox.sol
Outdated
* @param token The token withdrawn. | ||
* @param amount The amount withdrawn. | ||
*/ | ||
event Withdraw(address indexed token, uint256 amount); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should probably emit the recipient as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
Also added the sender
to the Deposit
event.
README.md
Outdated
There are 3 "roles" in this contract: | ||
|
||
1. `owner`: has full control of how much and when it can send assets to `recipient`. | ||
2. `custodian`: approves changes in the `recipient` address made by the `owner`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mmm namewise - this entity is not really storing/custodying the tokens, it can only block changing the recipient.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It has to be named like this for legal reasons.
src/Safebox.sol
Outdated
/// @notice Addresses with owner access on this contract. `wards[usr]` | ||
mapping(address => uint256) public wards; | ||
/// @notice Addresses with custodian access on this contract. `can[usr]` | ||
mapping(address => uint256) public can; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If custodian is effectively immutable and can only be set in the constructor why is this mapping needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah this is a good observation. Let's just make an immutable variable and remove the mapping.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Coinbase requested the ability to be able to rotate their keys, so this will be needed. I'm adding the functionality right now.
src/Safebox.sol
Outdated
/** | ||
* @notice Check if an address has `custodian` access on this contract. | ||
* @param usr The user address. | ||
* @return Whether `usr` is a ward or not. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
usr
=> custodian
src/Safebox.sol
Outdated
* @notice Removes a custodian from this contract. | ||
* @param usr The user address. | ||
*/ | ||
function removeCustodian(address usr) external override onlyCustodian { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that as with wards
behavior, also here if there is more than one custodian this will allow any custodian to remove any other.
So need to make sure this is wanted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that's the expected feature.
src/Safebox.sol
Outdated
* @param _recipient The new recipient being approved. | ||
*/ | ||
function approveChangeRecipient(address _recipient) external override { | ||
require(custodians[msg.sender] == 1, "Safebox/not-custodian"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be replaced with the onlyCustodian
modifier?
|
||
vat = VatAbstract(_vat); | ||
token = GemAbstract(_token); | ||
recipient = _recipient; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider emitting a RecipientChange
event (and maybe changing the event name to RecipientSet
or something similar).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All other events have "verb-like" structure (i.e.: Rely
, Deny
, AddCustodian
), I'm gonna use SetRecipient
for this.
constructor(address _vat, address _token, address _owner, address _custodian, address _recipient) { | ||
require(_recipient != address(0), "Safebox/invalid-recipient"); | ||
|
||
wards[_owner] = 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that common behavior for MakerDao contracts is to only rely msg.sender in the constructor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, we are aware of that, however, since Coinbase will be handling the deployment, we want to avoid them having to deny
themselves afterwards.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to clarify, we are gonna provide them the parameters for deployment and then verify the contract after it's deployed. If there is a mismatch, the verification will fail and we will know.
Also the variables are public, so they could be easily checked after deployment & verification.
src/Safebox.sol
Outdated
* @notice Deposits tokens into this contract. | ||
* @param amount The amount of tokens. | ||
*/ | ||
function deposit(uint256 amount) external override { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you plan to use this function for depositing from the swap/output conduit?
Seems like current code passes the funds by specifying the recipient of buyGem
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right, I'm gonna remove that.
The idea was to keep track of Deposit
events, but we can do the same by tracking Push
from the RwaSwapOutputConduit
contract.
`deposit` will most likely never be used, so we are removing it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey guys, here is my brief analysis of SafeBox. I have extensive experience with escrows so I am reviewing it from the perspective of an escrow in "Maker style" :)
Since I am reviewing this in isolation, context may be missing with regards to some comments, however I have made various points as documented below:
TLDR
- 'Dumb depositing' ERC20 poses risks. I will go into this in the Attack Vectors section at the end.
- Everything is
override
and I really don't think it should be (also nothing isvirtual
inSafeBoxLike.sol
) - Some of the names could be changed to be more like regular Daiwanese, especially the
addCustodian
stuff - The structure of the 2 step ownership makes checking for
address(0)
edge cases mandatory however allows for smart contract wallets / recipients. This is a contextual thing, I want to confirm that this is intended. IsOwner
andIsCustodian
seem redundant unless I'm missing some context here.- Comments and NATSPEC could do with trimming down depending on how
dss
-ish you want the code to look; this resembles how I wrote my own code prior to adapting to thedss
style so this is just a general comment.
Documentation
- This looks good, just needs updates when it's all complete.
- I would appreciate some notes on why
address(0)
is checked for (i.e. to allowpendingRecipient
to work only if it's initialised)
SafeBox.sol
- AGPL license OK
- All interfaces used
- Not immediately clear to me how the Abstracts are used with the
external view
return functions inSafeBoxLike.sol
but this is probably my lack of knowledge (have never tried to just return an imported interface before). Knowledge gap here so someone else needs to fill this in and make sure it's OK. VatAbstract
andGemAbstract
OK - do we really need to use dss interfaces for only 3 interfaces? Personally I would useVatLike
andGemLike
.- Mappings OK - fixed spacing
- Recipient and pending recipient vars OK
- Auth mapping OK
onlyCustodian
code OK - modifier name seems a bit out of place since it sits next toauth
- not a big issue- Constructor OK
- Rely owner OK
- Add custodian OK
I don't likeAddCustodian
as a name when we haverely
already used as a keyword here. - Set recipient OK
- Set vat OK
- Set gem OK
I would change the param order to match either when they're used (sequentially) or make it alphabetically ordered, but that's me.
withdraw
has an attack vector explained below but the code is OK.rely
- why is this override?deny
- same issuefile
is OK - I made itpublic
and notoverride
but this is optional as it varies between implementations
Theaddress(0)
check is needed to stop issues withpendingRecipient
and this should be noted in documentation.isOwner
- why do we need this? Can't they just querywards[usr]
? I don't see a need for this (I may be missing context?) but the code is OK.isCustodian
- same asisOwner
addCustodian
OK but should be calledrelyCustodian
removeCustodian
OK but should be calleddenyCustodian
approveChangeRecipient
- so I have some notes on this:
Existing implementation seems OK as it is. The benefit of it is that it doesn't rely on a call from the recipient's address. The drawback is that we have to ensure that the recipient isn'taddress(0)
for this to be possible.
OZ has a 2 step transfer method however that requires that the recipient calls the accept function which works for EOAs but not things like smart contract wallets. In the end this depends on the usecase and I may be missing the context here.- No
deposit
function is present anymore (the previous deposit function looked OK though)
Attack Vectors
Assume that I am a malicious member of governance and decide to steal the funds of the SafeBox. I can use CREATE2crunch or another similar tool to determine the salt for a contract address which resembles an ERC20 token's address, then deploy my own ERC20 there which looks like the real thing, however, my transfer
function is malicious.
While a deposit
function would only transfer in tokens with address token
(meaning that deposit
and withdraw
keeps a 'closed loop' such that the deposited token is the one being withdrawn), depositing directly to the contract would allow this kind of attack to occur if there was someone malicious responsible for the SafeBox deployment.
src/Safebox.sol
Outdated
GemAbstract public immutable override token; | ||
|
||
/// @notice Addresses with owner access on this contract. `wards[usr]` | ||
mapping(address => uint256) public override wards; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Spacing
mapping(address => uint256) public override wards; | |
mapping (address => uint256) public override wards; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is handled by prettier-plugin-solidity
.
src/Safebox.sol
Outdated
/// @notice Addresses with owner access on this contract. `wards[usr]` | ||
mapping(address => uint256) public override wards; | ||
/// @notice Addresses with custodian access on this contract. `custodians[usr]` | ||
mapping(address => uint256) public override custodians; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Spacing
mapping(address => uint256) public override custodians; | |
mapping (address => uint256) public override custodians; |
src/Safebox.sol
Outdated
/// @notice Reference to the new recipient when it is to be changed. | ||
address public override pendingRecipient; | ||
|
||
modifier auth() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not crazy on the layout of code but it's subjective...
Maybe we should do it like autoline? (https://github.com/makerdao/dss-auto-line/blob/master/src/DssAutoLine.sol) where the rely deny and auth are together in the code? Up to you
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're using CES layout for this code.
* @param _custodian The safebox custodian. | ||
* @param _recipient The recipient for tokens in the safebox. | ||
*/ | ||
constructor(address _vat, address _token, address _owner, address _custodian, address _recipient) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason for the ordering of the params here? Doesn't seem to follow any specific logic - up to you here but I would order it either alphabetically or based on order of use within the constructor (owner custodian recipient vat token). Again up to you
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're trying to logically group them:
MCD core modules > General contracts > Roles for this contract.
emit Rely(_owner); | ||
|
||
custodians[_custodian] = 1; | ||
emit AddCustodian(_custodian); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not RelyCustodian
to keep it in line with existing naming conventions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're refraining from using Daiwanese for functions that are exclusively accessible to Coinbase.
src/Safebox.sol
Outdated
* @notice Grants `usr` owner access to this contract. | ||
* @param usr The user address. | ||
*/ | ||
function rely(address usr) external override auth { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
function rely(address usr) external override auth { | |
function rely(address usr) external auth { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
override
is here to ensure it's implementing the same method declared in the SafeboxLike
interface.
src/Safebox.sol
Outdated
* @notice Revokes `usr` owner access from this contract. | ||
* @param usr The user address. | ||
*/ | ||
function deny(address usr) external override auth { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
function deny(address usr) external override auth { | |
function deny(address usr) external auth { |
src/Safebox.sol
Outdated
* @param what The changed parameter name. `"recipient"` | ||
* @param data The new value of the parameter. | ||
*/ | ||
function file(bytes32 what, address data) external override auth { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make this public
and no override?
function file(bytes32 what, address data) external override auth { | |
function file(bytes32 what, address data) public auth { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why public
instead of external
?
README.md
Outdated
There are 2 possible ways of making a deposit into the `Safebox`: | ||
|
||
1. Use the `Safebox` address as the `to` address of an ERC-20 compatible `transfer` transaction. | ||
2. Call the `deposit`<sup>[1]</sup> method from the `Safebox` contract: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this is removed we should remove this :)
@amusingaxl can you please point us to the code of the input conduit with the support for post shutdown collateral distribution (basically the intended |
Thank you for the thorough review @The-Arbiter. Just providing some context: We are between a rock and a hard place in a sense that we want to use the regular Daiwanese where it's possible, however there was some push back on that from the Coinbase side. This contract is going to be operated by both MakerDAO Governance and Coinbase in a collaborative manner, so our strategy is trying to segregate the interfaces that each party will interact with. That's why we kept things like Contrary to most MCD contract out there, a ward is not able to add or remove "custodian" privileges, only another custodian can do so. That was explicitly requested by Coinbase, because they were not comfortable with the idea of MakerDAO Governance being freely able to replace them. Now answering to some of your comments:
In previous Solidity version you were required to
As explained above, we are using Daiwanese only for MakerDAO-facing features, as this was a requirement from Coinbase.
For this specific deal, the recipient is going to be a smart contract (a
These are mainly for Coinbase's use. While the
Our understanding is that this is not a Core MCD contract, so it would be fine to use CES coding convention here. Again, it's not something that would be a blocker for us, however considering the Coinbase requirements, I find it very hard to make it 100% compliant anyways.
Noted! Will update the docs.
From an ABI perspective, those types are simply translated to
Those interfaces are pretty much set in stone at this point. We import the specific files directly, so it wouldn't be a problem of bloating contract source verification for instance. We could make some copy-pasta from
I've been back and forth with the layout of this contract a couple of times TBH 😅.
Yes, this is by design. Recipient is not meant to play an active role here because. Regarding the possible attack vector:
Wouldn't this be solved by having the diligence of checking the We can even safeguard against this in the spell itself by having a sanity check like: require(address(safebox.token()) == chainlog.getAddress("<TOKEN>"), "Invalid safebox token"); Any other asset sent to this contract other than |
I'm finishing the required changes and will post the link to it here as soon as I'm done. |
@The-Arbiter @rockyfour here's the input conduit that is going to be used as |
Adds custodial deny
A safebox for digital assets.
owner
can request funds to be sent to therecipient
.custodian
can deny the request within a predefined time period (24h).custodian
cooperation is required whenever theowner
wants to update therecipient
.