The Notary is a contract that holds permissions for each critical wallet action. Where-as with a Gnosis Safe that has a group of owners and optional modules, Locksmith uses a progressively granular permission scheme that allows for maximum flexibility while maintaining a single integration pattern.
The Notary is designed to approve actions that operate on a wallet's state, like moving collateral or firing events. To approve any action the notary must determine that the action meets acceptable parameters as outlined by a wallet root key holder.
As of the first version, the Notary includes the following permissions:
Collateral Providers: Contract addresses that adhere to the ICollateralProvider interface and are trusted by the wallet's root key-holder to custody funds safely on-chain.
Scribes: Contract Addresses that are trusted to move funds on the Ledger between keys in a wallet.
Dispatchers: Contract addresses that are trusted to register and fire events within the wallet to the TrustEventLog.
These modules are entrusted by the root key holder to provide and extend the wallet's capacity to store, move, and automate funds. The same contract can hold multiple permissions but are acquired and set separately for composability.
Storage
The 'ledger' component in these mappings is the message sender that is querying for notarization. In the current implementation this is the Ledger and the TrustEventLog.
// the notary only respects one locksmithILocksmith public locksmith;// Key-holders enable collateral to be withdrawn from// the ledger.// ledgerAddress / keyId / providerAddress / arn => approvedAmountmapping(address=>mapping(uint256=>mapping(address=>mapping(bytes32=>uint256)))) public withdrawalAllowances;// trusted ledger actors// ledger / trust / role => [actors]mapping(address=>mapping(uint256=>mapping(uint8=> EnumerableSet.AddressSet))) private actorRegistry;// actor aliases// ledger / trust / role / actor => aliasmapping(address=>mapping(uint256=>mapping(uint8=>mapping(address=>bytes32)))) public actorAliases;// The notary cares about a few different role types// that are attached to the ledger/trust pair. This// enum differentiates the storage while still making// the entire relationship state directly queryable outside// the contract.uint8constantpublic COLLATERAL_PROVIDER =0;uint8constantpublic SCRIBE =1;uint8constantpublic EVENT_DISPATCHER =2;
locksmith
The reference to the Locksmith interface. Instead of using a contract registry, each Locksmith contract takes a direct deployment dependency on a contract it requires to operate.
withdrawalAllowances
A key holder will go to the collateral provider to request a withdrawal. For collateral providers to offer a valid withdrawal, it must be fully registered and balanced on the ledger and pass notarization. Since the collateral provider does not explicitly hold the key, a critical part of withdrawal notarization is to ensure the key-holder who is withdrawing has attested to approve that amount, of that asset, from that ledger, for that collateral provider. At later layers this is composed into a single transaction.
actorRegistry
A mapping of contract address of modules that are approved for each trust model, for each role. The roles are defined as simple unsigned integers and are documented below. These are checked during operations to ensure the actors are considered trusted.
actorAliases
Human readable alises for each contract address and role. Because no off-chain infrastructure should be needed to create a feasible UX, we are paying the gas and the storage to provide some sane expectations for clients.
COLLATERAL_PROVIDER
This role describes an actor that is trusted by the root key holder to honor asset deposit and withdrawal requests from key holders, respecting the key-holder balances on the wallet's central Ledger. This enables wallet owners to compose their asset storage and investments with different features and providers directly into their wallet. On-chain Vaults, Staking providers, or exchanges can expose on-chain ICollateralProvider interfaces and honor key-rights. This in effect brings all deployed assets back into a single virtual wallet.
SCRIBE
This role describes an actor that is trusted by the root key holder to move funds between keys. A scribe has the ability to move any funds on the ledger between any set of valid keys within the wallet's trust model. It is up to the root key holder to sufficiently trust the means by which the scribe will move funds. Contracts that restrict access to move funds under different scenarios can be composed.
DISPATCHER
This role describes an actor that is trusted by the root key holder to register and fire events. Events are immutable boolean triggers that can be consumed by collateral providers, scribes, or other applications to compose additional logic and gates into features.
Operations
There are two typese of operations. There are operations that can only be successfully executed by a root key holder, and there are operations that are assumed to be executed by the associated role's ledger.
setTrustedLedgerRole
This method is called by a root key holder to specify to trust or untrust a specific contract address for a given role. It ensures that the root key holder is valid, and then will double check the users action before storing the root key's configuration.
This method can only be successfully called by a root key holder.
/** * setTrustedLedgerRole * * @param rootKeyId the root key the caller is trying to use to enable an actor * @param role the role the actor will play (provider or scribe) * @param ledger the contract of the ledger used by the actor * @param actor the contract of the ledger actor * @param trustLevel the flag to set the trusted status of this actor * @param actorAlias the alias of the actor, set if the trustLevel is true */functionsetTrustedLedgerRole(uint256 rootKeyId,uint8 role,address ledger,address actor,bool trustLevel,bytes32 actorAlias) external {// make sure that the caller is holding the key they are trying to userequire(IKeyVault(locksmith.getKeyVault()).keyBalanceOf(msg.sender, rootKeyId,false) >0,"KEY_NOT_HELD");// make sure the key is a valid root keyrequire(locksmith.isRootKey(rootKeyId),"KEY_NOT_ROOT");// the caller is holding it a valid root key, this lookup is safe (,,uint256 trustId,,) = locksmith.inspectKey(rootKeyId);if (trustLevel) {// make sure they are not already a provider on the trustrequire(!actorRegistry[ledger][trustId][role].contains(actor),'REDUNDANT_PROVISION');// register them with the trust if not already done so actorRegistry[ledger][trustId][role].add(actor);// set the alias actorAliases[ledger][trustId][role][actor] = actorAlias; } else {// we are trying to revoke status, so make sure they are onerequire(actorRegistry[ledger][trustId][role].contains(actor),'NOT_CURRENT_ACTOR');// remove them from the notary. At this point in time// there could still be collateral in the trust from this provider.// the provider isn't trusted at this moment to facilitate deposits// or withdrawals. Adding them back would re-enable their trusted// status. This is useful if a collateral provider is somehow compromised. actorRegistry[ledger][trustId][role].remove(actor); }}
setWithdrawalAllowance
All key holders must attest to the notary that collaterael providers are cleared to register withdrawals to the ledger on their behalf. This is because the collateral provider is not required to hold the key when facilitating a withdrawal. But in its place resides the key holder attestation to the notary.
A key holder calls this method, usually right before a withdrawal, to enable the collateral provider to successfully register on the ledger.
This method can only be successfully called by a key holder.
/** * setWithdrawalAllowance * * @param ledger address of the ledger to enable withdrawals from * @param provider collateral provider address to approve * @param keyId key ID to approve withdraws for * @param arn asset you want to approve withdrawal for * @param amount amount of asset to approve */functionsetWithdrawalAllowance(address ledger,address provider,uint256 keyId,bytes32 arn,uint256 amount) external {require(IKeyVault(locksmith.getKeyVault()).keyBalanceOf(msg.sender, keyId,false) >0,'KEY_NOT_HELD'); withdrawalAllowances[ledger][keyId][provider][arn] = amount; }
notarizeDeposit
The wallet's central ledger will call this for notarization when a collateral provider attempt to deposit to a wallet's ledger. This will fail if the key is invalid, or if the collateral provider is not trusted by the wallet's root key holder.
Below is also an internal method, requireTrustedActor(), that is used for multiple notarization operations.
/** * notarizeDeposit * * @param provider the provider that is trying to deposit * @param keyId key to deposit the funds to * @param arn asset resource hash of the withdrawn asset * @param amount the amount of that asset withdrawn. * @return the valid trust Id for the key */functionnotarizeDeposit(address provider,uint256 keyId,bytes32 arn,uint256 amount ) externalreturns (uint256) {uint256 trustId =requireTrustedActor(keyId, provider, COLLATERAL_PROVIDER);return trustId; }/** * requireTrustedActor * * Given a key and an actor, panic if the key isn't real, * it's not root when it needs to be, or the trust * doesn't trust the actor against a given ledger. * * This method assumes the message sender is the ledger. * * @param keyId the key Id for the operation * @param actor the actor address to check * @param role the role you need the actor to be trusted to play * @return the valid trust ID associated with the key */functionrequireTrustedActor(uint256 keyId,address actor,uint8 role ) internalviewreturns (uint256) {// make sure the key is valid. you can't always ensure// that the actor is checking this (bool valid,,uint256 trustId,,) = locksmith.inspectKey(keyId);require(valid,"INVALID_KEY");// make sure the actor is trusted// we assume the message sender is the ledgerrequire(actorRegistry[msg.sender][trustId][role].contains(actor),'UNTRUSTED_ACTOR');return trustId;}
notarizeWithdrawal
The wallet's central ledger will call this for notarization when a collateral provider attempt to withdrawal to a wallet's ledger. This will fail if the key is invalid, the withdrawal allowance for that key holder, provider, and asset is insufficient or if the collateral provider is not trusted by the wallet's root key holder.
The notary does not check the ledger for sufficient balance. The ledger will fail to create a valid entry on its own. The notaries role is the ensure everything else is proper.
/** * notarizeWithdrawal * * @param provider the provider that is trying to withdrawal * @param keyId key to withdrawal the funds from * @param arn asset resource hash of the withdrawn asset * @param amount the amount of that asset withdrawn. * @return the valid trust ID for the key */functionnotarizeWithdrawal(address provider,uint256 keyId,bytes32 arn,uint256 amount ) externalreturns (uint256) {// make sure the key is valid and the provider is trusteduint256 trustId =requireTrustedActor(keyId, provider, COLLATERAL_PROVIDER);// make sure the withdrawal amount is approved by the keyholder// and then reduce the amountrequire(withdrawalAllowances[msg.sender][keyId][provider][arn] >= amount,'UNAPPROVED_AMOUNT'); withdrawalAllowances[msg.sender][keyId][provider][arn] -= amount;return trustId; }
notarizeDistribution
This method is called by the ledger when a scribe is attempting to re-distribute funds amongst keys. The notarization will fail if the involved scribe or collateral provider isn't trusted, or if the keys do not all belong to the same ring.
/** * notarizeDistribution * * @param scribe the address of the scribe that is supposedly trusted * @param provider the address of the provider whose funds are to be moved * @param arn the arn of the asset being moved * @param sourceKeyId the root key that the funds are moving from * @param keys array of keys to move the funds to * @param amounts array of amounts corresponding for each destination keys * @return the trustID for the rootKey */functionnotarizeDistribution(address scribe,address provider,bytes32 arn,uint256 sourceKeyId,uint256[] calldata keys,uint256[] calldata amounts) externalreturns (uint256) {// the scribe needs to be trusteduint256 trustId =requireTrustedActor(sourceKeyId, scribe, SCRIBE);// we also want to make sure the provider is trustedrequire(actorRegistry[msg.sender][trustId][COLLATERAL_PROVIDER].contains(provider),'UNTRUSTED_PROVIDER');// check to ensure the array sizes are 1:1require(keys.length == amounts.length,"KEY_AMOUNT_SIZE_MISMATCH");// this method will fully panic if its not valid. locksmith.validateKeyRing(trustId, keys,true);return trustId; }
notarizeEventRegistration
This method is called by the TrustEventLog to ensure that a dispatcher whom is registering an event is trusted by the specified wallet owner. This prevents spam, attacks, or otherwise random events from popping up in the event stream. In this implementation, two default dispachers are included (KeyOracle and AlarmClock)./**
/** * notarizeEventRegistration * * @param dispatcher registration address origin * @param trustId the trust ID for the event * @param eventHash the unique event identifier * @param description the description of the event */ function notarizeEventRegistration(address dispatcher, uint256 trustId, bytes32 eventHash, bytes32 description) external {
// we want to make sure the dispatcher is trusted// note: here we are using the event log as the "ledger".require(actorRegistry[msg.sender][trustId][EVENT_DISPATCHER].contains(dispatcher),'UNTRUSTED_DISPATCHER'); }