description |
---|
Simple ERC-20 Storage |
ERC-20s operate differently than Ether. Their interface and protocol for moving funds is divergent enough from the gas token to warrant its own interface. As such, the TokenVault takes the same mode as EtherVault, with additions required for the ERC-20 standard.
// Locksmith verifies key-holdership.
ILocksmith public locksmith;
// The Locksmith provides access to mutate the ledger.
ILedger public ledger;
// witnessed token addresses
// trust => [registered addresses]
mapping(uint256 => EnumerableSet.AddressSet) private witnessedTokenAddresses;
mapping(bytes32 => address) public arnContracts;
// we need to keep track of the deposit balances safely
mapping(address => uint256) tokenBalances;
ARNs are designed to be opaque because they don't inform the asset's behavior, just its uniqueness. For that reason, the TokenVault ensures that it keeps track of every ERC20 token address it's witnessed for each individual trust model.
During the deposit flow we compute the ARN using the token contract address. We store a mapping between the ARN and the contract for easy access.
Similar in nature to EtherVault's etherBalance, is tracked for each token address.
The operations support the ability to deposit and withdrawal directly or through the ICollateralProvider
ARN interface.
The message sender calls this after approving the vault's contract the proper amount to move funds from the sender to the contract. The contract ensures the caller is holding the claimed key, ensures the message caller is properly funded and then moves the assets into the vault. After they safely arrive, the deposit is recorded on the ledger. This operation will fail if the vault isn't trusted.
/**
* deposit
*
* @param keyId the ID of the key that the depositor is using.
* @param token the address of the ERC20 token contract.
* @param amount the amount to deposit
*/
function deposit(uint256 keyId, address token, uint256 amount) external {
// stop right now if the message sender doesn't hold the key
require(IKeyVault(locksmith.getKeyVault()).keyBalanceOf(msg.sender, keyId, false) > 0, 'KEY_NOT_HELD');
// generate the token arn
bytes32 tokenArn = AssetResourceName.AssetType({
contractAddress: token,
tokenStandard: 20,
id: 0
}).arn();
// store the contract address to enable withdrawals
arnContracts[tokenArn] = token;
// make sure the caller has a sufficient token balance.
require(IERC20(token).balanceOf(msg.sender) >= amount,
"INSUFFICIENT_TOKENS");
// transfer tokens in the target token contract. if the
// control flow ever got back into the callers hands
// before modifying the ledger we could end up re-entrant.
IERC20(token).transferFrom(msg.sender, address(this), amount);
// track the deposit on the ledger
(,,uint256 finalLedgerBalance) = ledger.deposit(keyId, tokenArn, amount);
// increment the witnessed token balance
tokenBalances[token] += amount;
// jam the vault if the ledger's balance
// provisions doesn't match the vault balance
assert(finalLedgerBalance == tokenBalances[token]);
// update the witnessed token addresses, so we can easily describe
// the trust-level tokens in this vault.
(,,uint256 trustId,,) = locksmith.inspectKey(keyId);
witnessedTokenAddresses[trustId].add(token);
}
The withdrawal method supports multiple interfaces because it is a multi-asset vault. For this, it facilitates both the direct ERC20 token withdrawal interface, and the ARN-based interface for wallet orchestration.
The direct token method ultimately computes the ARN and calls the internal method.
/**
* withdrawal
*
* Given a key, attempt to withdrawal ERC20 from the vault. This will only
* succeed if the key is held by the user, the key has the permission to
* withdrawal, the rules of the trust are satisified (whatever those may be),
* and there is sufficient balance. If any of those fail, the entire
* transaction will revert and fail.
*
* @param keyId the key you want to use to withdrawal with/from
* @param token the token contract representing the ERC20
* @param amount the amount of ether, in gwei, to withdrawal from the balance.
*/
function withdrawal(uint256 keyId, address token, uint256 amount) external {
// generate the ARN, and then withdrawal
_withdrawal(keyId, AssetResourceName.AssetType({
contractAddress: token,
tokenStandard: 20,
id: 0
}).arn(), token, amount);
}
The ICollateralProvider
interface method is a direct bridge to the internal implementation:
function arnWithdrawal(uint256 keyId, bytes32 arn, uint256 amount) external {
// grab the address for the contract. If this ends up being address(0), the
// ledger should fail to withdrawal, so there is no need to check it here
_withdrawal(keyId, arn, arnContracts[arn], amount);
}
With the implementation that feeds both interfaces here:
/**
* _withdrawal
*
* @param keyId the key to withdrawal from the ledger
* @param arn the asset idenitifier to withdrawal from the ledger
* @param token the token address to use to move assets.
* @param amount the amount of assets to remove from the ledger, and send.
*/
function _withdrawal(uint256 keyId, bytes32 arn, address token, uint256 amount) internal {
// stop right now if the message sender doesn't hold the key
require(IKeyVault(locksmith.getKeyVault()).keyBalanceOf(msg.sender, keyId, false) > 0, 'KEY_NOT_HELD');
// withdrawal from the ledger *first*. if there is an overdraft,
// the entire transaction will revert.
(,, uint256 finalLedgerBalance) = ledger.withdrawal(keyId, arn, amount);
// decrement the witnessed token balance
tokenBalances[token] -= amount;
// jam the vault if the ledger's balance doesn't
// match the vault balance after withdrawal
assert(tokenBalances[token] == finalLedgerBalance);
// We trust that the ledger didn't overdraft so
// send at the end to prevent re-entrancy.
IERC20(token).transfer(msg.sender, amount);
}
The TokenVault
also provides an introspection method to audit all of the token addresses that have been witnessed as deposits for a given trust model.
/**
* getTokenTypes
*
* Given a specific key, will return the contract addresses for all
* ERC20s held in the vault.
*
* @param keyId the key you are using to access the trust
* @return the token registry for that trust
*/
function getTokenTypes(uint256 keyId) external view returns(address[] memory) {
(,,uint256 trustId,,) = locksmith.inspectKey(keyId);
return witnessedTokenAddresses[trustId].values();
}