Delegate.cash is a new protocol intended to make airdrops and token claims safer. If you have vaulted NFTs, you strongly want to avoid using that vault wallet to interact with any contracts. And if you have to move NFTs back & forth between your vault wallet and hot wallet to make claims, then you probably won’t. foobar, one of the authors of delegate.cash, has a great write up on why this kind of protocol needs to exist. Let’s take a look at the code…
(note that the code blocks below contain some intermixed code from different parts of DelegationRegistry.sol and IDelegationRegistry.sol for clarity of explanation)
Delegation
There are 3 types of delegation:
ALL
– thevault
wallet delegates all actions to thedelegate
walletCONTRACT
– thevault
wallet delegates all actions on a specific contract to thedelegate
walletTOKEN
– thevault
wallet delegates all actions for a specific token (on a specific contract) to thedelegate
wallet
There are delegation functions for each of these delegation types that allow you to enable or disable the delegation (based on value
). These functions must be called from the vault
wallet (vault = msg.sender
), but they are quite straightforward and safe:
- Compute a
delegationHash
- Set
DelegationInfo
values for thatdelegationHash
enum DelegationType {
NONE,
ALL,
CONTRACT,
TOKEN
}
function delegateForAll(address delegate, bool value) external override {
bytes32 delegationHash = _computeAllDelegationHash(msg.sender, delegate);
_setDelegationValues(
delegate, delegationHash, value, IDelegationRegistry.DelegationType.ALL, msg.sender, address(0), 0
);
emit IDelegationRegistry.DelegateForAll(msg.sender, delegate, value);
}
function delegateForContract(address delegate, address contract_, bool value) external override {
bytes32 delegationHash = _computeContractDelegationHash(msg.sender, delegate, contract_);
_setDelegationValues(
delegate, delegationHash, value, IDelegationRegistry.DelegationType.CONTRACT, msg.sender, contract_, 0
);
emit IDelegationRegistry.DelegateForContract(msg.sender, delegate, contract_, value);
}
function delegateForToken(address delegate, address contract_, uint256 tokenId, bool value) external override {
bytes32 delegationHash = _computeTokenDelegationHash(msg.sender, delegate, contract_, tokenId);
_setDelegationValues(
delegate, delegationHash, value, IDelegationRegistry.DelegationType.TOKEN, msg.sender, contract_, tokenId
);
emit IDelegationRegistry.DelegateForToken(msg.sender, delegate, contract_, tokenId, value);
}
Delegation Hash
The delegationHash
is where the authors got clever. Not only does the hash make delegation lookups faster and more efficient, it makes revoking delegations very cheap for gas. Every vault
wallet has a numeric vaultVersion
, which is initially 0
. If this gets changed, then the delegationHash
will change. There is a similar numeric delegateVersion
for (vault, delegate)
pairs. Revoking (covered more below) is just a matter of changing those version numbers, therefore changing the hash.
/// @notice A mapping of wallets to versions (for cheap revocation)
mapping(address => uint256) internal vaultVersion;
/// @notice A mapping of wallets to delegates to versions (for cheap revocation)
mapping(address => mapping(address => uint256)) internal delegateVersion;
function delegateForAll(address delegate, bool value) external override {
bytes32 delegationHash = _computeAllDelegationHash(msg.sender, delegate);
_setDelegationValues(
delegate, delegationHash, value, IDelegationRegistry.DelegationType.ALL, msg.sender, address(0), 0
);
emit IDelegationRegistry.DelegateForAll(msg.sender, delegate, value);
}
function delegateForContract(address delegate, address contract_, bool value) external override {
bytes32 delegationHash = _computeContractDelegationHash(msg.sender, delegate, contract_);
_setDelegationValues(
delegate, delegationHash, value, IDelegationRegistry.DelegationType.CONTRACT, msg.sender, contract_, 0
);
emit IDelegationRegistry.DelegateForContract(msg.sender, delegate, contract_, value);
}
function delegateForToken(address delegate, address contract_, uint256 tokenId, bool value) external override {
bytes32 delegationHash = _computeTokenDelegationHash(msg.sender, delegate, contract_, tokenId);
_setDelegationValues(
delegate, delegationHash, value, IDelegationRegistry.DelegationType.TOKEN, msg.sender, contract_, tokenId
);
emit IDelegationRegistry.DelegateForToken(msg.sender, delegate, contract_, tokenId, value);
}
Delegation Info
When delegating, a DelegationInfo
object is created and stored by delegateHash
. This DelegationInfo
contains the following data:
vault
: thevault
walletdelegate
: thedelegate
or hot wallettype_
: theDelegationType
, which can beALL
,CONTRACT
, orTOKEN
contract_
: the contract address forCONTRACT
&TOKEN
types, or the0
address forALL
tokenId
: the token number for theTOKEN
type, or0
If value
is false
, then the delegateHash
and corresponding DelegationInfo
are deleted, thereby revoking a specific delegation. However, there are cheaper ways to revoke delegation, covered next.
/// @notice Info about a single delegation, used for onchain enumeration
struct DelegationInfo {
DelegationType type_;
address vault;
address delegate;
address contract_;
uint256 tokenId;
}
function _setDelegationValues(
address delegate,
bytes32 delegateHash,
bool value,
IDelegationRegistry.DelegationType type_,
address vault,
address contract_,
uint256 tokenId
) internal {
if (value) {
delegations[vault][vaultVersion[vault]].add(delegateHash);
delegationHashes[delegate].add(delegateHash);
delegationInfo[delegateHash] =
DelegationInfo({vault: vault, delegate: delegate, type_: type_, contract_: contract_, tokenId: tokenId});
} else {
delegations[vault][vaultVersion[vault]].remove(delegateHash);
delegationHashes[delegate].remove(delegateHash);
delete delegationInfo[delegateHash];
}
}
Revoking
The contract provides very cheap ways to revoke delegation, by changing the vaultVersion
or delegateVersion
.
revokeAllDelegates
would be called from thevault
wallet, and invalidates all delegations for that walletrevokeDelegate
would also be called from thevault
wallet, to invalidate a specificdelegate
walletrevokeSelf
would be called from adelegate
wallet, to invalidate all delegations from a specificvault
wallet
As mentioned above, instead of a complicated and gas expensive deletion of DelegationInfo
, by incrementing a version number the delegation hash computation is changed. Technically this means the old DelegationInfo
remains in the contract storage, but it is no longer accessible and cannot be used by any delegation lookups. There is also a theoretical limit of 2^256-1
for the number of times you can revoke and change either of the version numbers, but you’d have to try extremely hard and spend a lot of gas to do this.
function revokeAllDelegates() external override {
++vaultVersion[msg.sender];
emit IDelegationRegistry.RevokeAllDelegates(msg.sender);
}
function revokeDelegate(address delegate) external override {
_revokeDelegate(delegate, msg.sender);
}
function revokeSelf(address vault) external override {
_revokeDelegate(msg.sender, vault);
}
function _revokeDelegate(address delegate, address vault) internal {
++delegateVersion[vault][delegate];
// For enumerations, filter in the view functions
emit IDelegationRegistry.RevokeDelegate(vault, msg.sender);
}
Delegation Lookups
There are a number of different ways to lookup delegations. I’ve included the function signatures below, but not the complete functions, as they are complicated, highly optimized loops iterating over the delegations created by the functions covered above. They are view
only functions and therefore safe to call.
function getDelegatesForAll(address vault)
external view returns (address[] memory delegates)
function getDelegatesForContract(address vault, address contract_)
external view override returns (address[] memory delegates)
function getDelegatesForToken(address vault, address contract_, uint256 tokenId)
external view override returns (address[] memory delegates)
function getContractLevelDelegations(address vault)
external view returns (IDelegationRegistry.ContractDelegation[] memory contractDelegations)
function getTokenLevelDelegations(address vault)
external view returns (IDelegationRegistry.TokenDelegation[] memory tokenDelegations)
function checkDelegateForAll(address delegate, address vault)
public view override returns (bool)
function checkDelegateForContract(address delegate, address vault, address contract_)
public view override returns (bool)
function checkDelegateForToken(address delegate, address vault, address contract_, uint256 tokenId)
public view override returns (bool)
These functions are designed to be called from other contracts or dapps, to enable the delegation protocol. For example, if a delegate
wallet wants to make a claim for a NFT, it could provide a vault
wallet and tokenId
to the claim contract. The claim contract would then do the following:
- Confirm the
vault
wallet ownstokenId
- Call
checkDelegateForToken(delegate, vault, contract_, tokenId)
- Proceed with the claim if
checkDelegateForToken
returnstrue
- Deny the claim if
checkDelegateForToken
returnsfalse
For the best UX, you’d probably want to call these functions initially from the dapp, but for security, you definitely want to verify everything within the claim contract.
Slither Analysis
Unfortunately, slither 0x00000000000076A84feF008CDAbe6409d2FE638B
doesn’t work because the openzepplin contract dependency paths are different than how etherscan structures things. To make slither work, I did the following:
git clone --recurse-submodules https://github.com/0xfoobar/delegation-registry.git
cd delegation-registry/lib
slither ../src/DelegationRegistry.sol --print human-summary
slither ../src/DelegationRegistry.sol

As you can see, it’s in pretty good shape. The 4 medium issues are all unused returns in _setDelegationValues
when adding or removing delegation hashes from a set. The add
and remove
functions just return a bool
if the set was changed, which doesn’t really matter for this contract, so ignoring the return value is totally fine.
The 1 optimization issue is that checkDelegateForToken
should be declared external. It is actually external
in the IDelegationRegistry
interface, so I’m not sure why it’s public
in DelegationRegistry
. Maybe it’s just a simple oversight, but not really a big deal either.
Some of the informational issues are about inline assembly in some of the delegation lookups. Assembly should definitely be used with care. In this case, the assembly is in view
only functions, in a way that helps reduce the computation cost.
Conclusion
Delegate.cash does exactly what it claims to, in a very straightforward way. However, it’s useless on its own. For this protocol to work, developers have to start including it in their own contracts and dapps, then recommending it to their users. The team behind delegate.cash are attempting to make an official standard with EIP-5639. But the best thing they can do is to provide easy to use libraries and sample code for integrating the protocol into other contracts and dapps. Ideally this would go as far as re-usable React components and a sample claim contract to demonstrate an entire user & wallet flow. If developers can essentially copy & paste to give their users more secure options, then adoption will be much easier.