Tag Archives: airdrop

Vault

Delegate.cash Contract Review

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:

  1. ALL – the vault wallet delegates all actions to the delegate wallet
  2. CONTRACT – the vault wallet delegates all actions on a specific contract to the delegate wallet
  3. TOKEN – the vault wallet delegates all actions for a specific token (on a specific contract) to the delegate 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:

  1. Compute a delegationHash
  2. Set DelegationInfo values for that delegationHash
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: the vault wallet
  • delegate: the delegate or hot wallet
  • type_: the DelegationType, which can be ALL, CONTRACT, or TOKEN
  • contract_: the contract address for CONTRACT & TOKEN types, or the 0 address for ALL
  • tokenId: the token number for the TOKEN type, or 0

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 the vault wallet, and invalidates all delegations for that wallet
  • revokeDelegate would also be called from the vault wallet, to invalidate a specific delegate wallet
  • revokeSelf would be called from a delegate wallet, to invalidate all delegations from a specific vault 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:

  1. Confirm the vault wallet owns tokenId
  2. Call checkDelegateForToken(delegate, vault, contract_, tokenId)
  3. Proceed with the claim if checkDelegateForToken returns true
  4. Deny the claim if checkDelegateForToken returns false

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:

  1. git clone --recurse-submodules https://github.com/0xfoobar/delegation-registry.git
  2. cd delegation-registry/lib
  3. slither ../src/DelegationRegistry.sol --print human-summary
  4. slither ../src/DelegationRegistry.sol
Human summary of slither analysis

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.