Skip to content

mobilecoinofficial/reserve-auditor

This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Folders and files

NameName
Last commit message
Last commit date

Latest commit

b8c94c9 · Apr 4, 2023

History

28 Commits
Nov 11, 2022
Apr 4, 2023
Apr 4, 2023
Mar 23, 2023
Nov 11, 2022
Mar 14, 2023
Mar 23, 2023
Nov 11, 2022
Apr 4, 2023
Mar 23, 2023
Nov 11, 2022
Nov 11, 2022
Nov 11, 2022
Nov 11, 2022
Nov 11, 2022
Nov 11, 2022
Nov 11, 2022
Mar 23, 2023
Mar 23, 2023
Feb 8, 2023
Nov 11, 2022
Nov 11, 2022
Nov 11, 2022
Nov 11, 2022
Feb 24, 2023
Nov 11, 2022
Feb 24, 2023
Mar 14, 2023
Feb 24, 2023
Nov 11, 2022
Feb 27, 2023

Repository files navigation

reserve-auditor

This is a service which provides a gRPC API for auditing mints and burns on the MobileCoin blockchain, and optionally correlating them with deposits and withdrawals on a Gnosis Safe.

The reserve auditor stores its audit information in a SQLite database, and provides a gRPC for querying this database. It also provides some Prometheus metrics to ease automated monitoring.

Running the reserve auditor

Full use of the Reserve Auditor requires a local ledger of the mobilecoin network, a scanning layer to ingest the contents of the local ledger, an http server to serve the results to http://localhost:3000, and a front end to visualize the data. See BUILD.md for more information

Gnosis Safe Auditing

The reserve auditor supports syncing data from a Gnosis safe. It uses the Gnosis transaction service API to get the data. This service is operated by Gnosis, and is available for ETH main net and Goerli, an ETH test net.

Mints on the MobileCoin blockchain are expected to correlate with a deposit to a safe. The expected process is:

  1. A deposit of the appropriate backing token is made to the safe using a standard Ethereum transaction.
  2. A MintTx is then submitted to the MobileCoin blockchain, embedding the deposit transaction hash in the nonce of the MobileCoin MintTx. The nonce allows linking the MobileCoin mint to the Gnosis safe deposit.

Similarly, burns on the MobileCoin blockchain are expected to correlate with a withdrawal from a safe. The expected process is:

  1. A transaction on the MobileCoin blockchain that moves the desired token to the burn address is issued.
  2. A batched transaction is issued to the Ethereum blockchain. The batched transaction needs to contain two transactions:
    1. A transaction that moves the desired token out of the safe
    2. A transaction to an auxiliary contract (see more details below) that is used to link withdrawal to the MobileCoin burn.

Gnosis deposits are easily linked to the matching MobileCoin mints via the Ethereum transaction hash. Linking withdrawals is more difficult since standard Ethereum transactions do not have a way of including metadata. In an ideal world we would've had the option of including the MobileCoin burn transaction TxOut public key in the Ethereum withdrawal transaction, but there is no easy way to do that. The solution we came up with is to deploy an "auxiliary contract", who has a single function that accepts arbitrary metadata bytes, and use that as part of a Gnosis batched transfer to include extra data in addition to the token transfer. Such contract can be seen here and is deployed to the Goerli network.

Setting up

The first step is to decide which asset you are going to use on the Ethereum blockchain, and get some ETH (for paying gas fees) and some of this test asset. For testing purposes we have used gRSV (Goerli RSV) - https://goerli.etherscan.io/token/0xeC76FbFD75481839e456C4cb2cd23cda813f19B1. You need to get some ETH, and some of this asset. Google around to find working faucets. At the time of writing, https://goerlifaucet.com/ and https://goerli-faucet.mudit.blog/ worked.

To play around with Gnosis auditing the first step is to create a safe. This can be done on https://gnosis-safe.io/ Once the safe is created, it will be assigned an address on the Ethereum blockchain. This assumes you have a wallet that your browser can connect to such as MetaMask. Note that creating a Gnosis Safe requires submitting a transaction to the Ethereum blockchain, so your wallet will need to have some ETH to pay the gas fees.

The Ethereum network you are testing with will need to have the metadata contract deployed. For the Goerli test network, this was already done and assigned the address 0x89d2c2C7853AD6015d50f39A615196e17f13Cf59. You will also need to know the 4 byte signature of the metadata contract emitBytes function. For the contract mentioned above, this is AUX_BURN_FUNCTION_SIG.to_vec() (since this is derived from the function signature, it should be the same for all unmodified deployments of this contract).

Depositing to the safe

Depositing to the safe is as simple as sending a standard Ethereum transaction that moves your desired token (gRSV in this example) into the safe's address.

Once the Gnosis transaction service notices the transaction and the auditor syncs it you should see a log message similar to this: 2022-06-21 20:40:42.785395662 UTC INFO Processing gnosis safe deposit: EthereumTransfer { from: EthAddr("0xdc079a637a1417020916FfB8a39fF5a2801A0F07"), to: EthAddr("0xeC018400FFe5Ad6E0B42Aa592Ee1CF6092972dEe"), token_addr: Some(EthAddr("0xeC76FbFD75481839e456C4cb2cd23cda813f19B1")), tx_hash: EthTxHash("0x744372bb82b2d0f0e7b2722d163ffef97656562b40cc7fad9a1809d14aaf626a"), tx_type: "ERC20_TRANSFER", value: JsonU64(10000000000000000000) }, mc.app: mc-reserve-auditor, mc.module: mc_reserve_auditor::gnosis::sync, mc.src: reserve-auditor/src/gnosis/sync.rs:128

Withdrawing from the safe

Withdrawal is slightly move involved since you will need to construct a multi-transaction that both moves the token out of the safe and transacts with the auxiliary metadata contracts to record the matching MobileCoin burn TxOut public key.

The steps to do that are:

  1. On the Gnosis safe web app click Apps and then select the Transaction Builder application.
  2. We will first construct the transaction that moves the RinkUSDT token. In the Enter Address or ENS Name you need to put the contract address, which for gRSV on Goerli is 0xeC76FbFD75481839e456C4cb2cd23cda813f19B1
  3. Under Enter ABI you need to put the contract ABI
View Contract
[
  {
    "constant": true,
    "inputs": [],
    "name": "name",
    "outputs": [{ "name": "", "type": "string" }],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  {
    "constant": false,
    "inputs": [{ "name": "_upgradedAddress", "type": "address" }],
    "name": "deprecate",
    "outputs": [],
    "payable": false,
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "constant": false,
    "inputs": [
      { "name": "_spender", "type": "address" },
      { "name": "_value", "type": "uint256" }
    ],
    "name": "approve",
    "outputs": [],
    "payable": false,
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [],
    "name": "deprecated",
    "outputs": [{ "name": "", "type": "bool" }],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  {
    "constant": false,
    "inputs": [{ "name": "_evilUser", "type": "address" }],
    "name": "addBlackList",
    "outputs": [],
    "payable": false,
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [],
    "name": "totalSupply",
    "outputs": [{ "name": "", "type": "uint256" }],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  {
    "constant": false,
    "inputs": [
      { "name": "_from", "type": "address" },
      { "name": "_to", "type": "address" },
      { "name": "_value", "type": "uint256" }
    ],
    "name": "transferFrom",
    "outputs": [],
    "payable": false,
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [],
    "name": "upgradedAddress",
    "outputs": [{ "name": "", "type": "address" }],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [{ "name": "", "type": "address" }],
    "name": "balances",
    "outputs": [{ "name": "", "type": "uint256" }],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [],
    "name": "decimals",
    "outputs": [{ "name": "", "type": "uint256" }],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [],
    "name": "maximumFee",
    "outputs": [{ "name": "", "type": "uint256" }],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [],
    "name": "_totalSupply",
    "outputs": [{ "name": "", "type": "uint256" }],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  {
    "constant": false,
    "inputs": [],
    "name": "unpause",
    "outputs": [],
    "payable": false,
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [{ "name": "_maker", "type": "address" }],
    "name": "getBlackListStatus",
    "outputs": [{ "name": "", "type": "bool" }],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [
      { "name": "", "type": "address" },
      { "name": "", "type": "address" }
    ],
    "name": "allowed",
    "outputs": [{ "name": "", "type": "uint256" }],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [],
    "name": "paused",
    "outputs": [{ "name": "", "type": "bool" }],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [{ "name": "who", "type": "address" }],
    "name": "balanceOf",
    "outputs": [{ "name": "", "type": "uint256" }],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  {
    "constant": false,
    "inputs": [],
    "name": "pause",
    "outputs": [],
    "payable": false,
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [],
    "name": "getOwner",
    "outputs": [{ "name": "", "type": "address" }],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [],
    "name": "owner",
    "outputs": [{ "name": "", "type": "address" }],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [],
    "name": "symbol",
    "outputs": [{ "name": "", "type": "string" }],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  {
    "constant": false,
    "inputs": [
      { "name": "_to", "type": "address" },
      { "name": "_value", "type": "uint256" }
    ],
    "name": "transfer",
    "outputs": [],
    "payable": false,
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "constant": false,
    "inputs": [
      { "name": "newBasisPoints", "type": "uint256" },
      { "name": "newMaxFee", "type": "uint256" }
    ],
    "name": "setParams",
    "outputs": [],
    "payable": false,
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "constant": false,
    "inputs": [{ "name": "amount", "type": "uint256" }],
    "name": "issue",
    "outputs": [],
    "payable": false,
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "constant": false,
    "inputs": [{ "name": "amount", "type": "uint256" }],
    "name": "redeem",
    "outputs": [],
    "payable": false,
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [
      { "name": "_owner", "type": "address" },
      { "name": "_spender", "type": "address" }
    ],
    "name": "allowance",
    "outputs": [{ "name": "remaining", "type": "uint256" }],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [],
    "name": "basisPointsRate",
    "outputs": [{ "name": "", "type": "uint256" }],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [{ "name": "", "type": "address" }],
    "name": "isBlackListed",
    "outputs": [{ "name": "", "type": "bool" }],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  {
    "constant": false,
    "inputs": [{ "name": "_clearedUser", "type": "address" }],
    "name": "removeBlackList",
    "outputs": [],
    "payable": false,
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [],
    "name": "MAX_UINT",
    "outputs": [{ "name": "", "type": "uint256" }],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  {
    "constant": false,
    "inputs": [{ "name": "newOwner", "type": "address" }],
    "name": "transferOwnership",
    "outputs": [],
    "payable": false,
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "constant": false,
    "inputs": [{ "name": "_blackListedUser", "type": "address" }],
    "name": "destroyBlackFunds",
    "outputs": [],
    "payable": false,
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      { "name": "_initialSupply", "type": "uint256" },
      { "name": "_name", "type": "string" },
      { "name": "_symbol", "type": "string" },
      { "name": "_decimals", "type": "uint256" }
    ],
    "payable": false,
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "anonymous": false,
    "inputs": [{ "indexed": false, "name": "amount", "type": "uint256" }],
    "name": "Issue",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [{ "indexed": false, "name": "amount", "type": "uint256" }],
    "name": "Redeem",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [{ "indexed": false, "name": "newAddress", "type": "address" }],
    "name": "Deprecate",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      { "indexed": false, "name": "feeBasisPoints", "type": "uint256" },
      { "indexed": false, "name": "maxFee", "type": "uint256" }
    ],
    "name": "Params",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      { "indexed": false, "name": "_blackListedUser", "type": "address" },
      { "indexed": false, "name": "_balance", "type": "uint256" }
    ],
    "name": "DestroyedBlackFunds",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [{ "indexed": false, "name": "_user", "type": "address" }],
    "name": "AddedBlackList",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [{ "indexed": false, "name": "_user", "type": "address" }],
    "name": "RemovedBlackList",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      { "indexed": true, "name": "owner", "type": "address" },
      { "indexed": true, "name": "spender", "type": "address" },
      { "indexed": false, "name": "value", "type": "uint256" }
    ],
    "name": "Approval",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      { "indexed": true, "name": "from", "type": "address" },
      { "indexed": true, "name": "to", "type": "address" },
      { "indexed": false, "name": "value", "type": "uint256" }
    ],
    "name": "Transfer",
    "type": "event"
  },
  { "anonymous": false, "inputs": [], "name": "Pause", "type": "event" },
  { "anonymous": false, "inputs": [], "name": "Unpause", "type": "event" }
]

https://goerli.etherscan.io/token/0x01812c3856442d697AFCa11E97dCad64522a8856#code contains a Contract ABI section that you copy-paste from.

4. Once you put the contract ABI, under the `Transaction information` section you should be able to select the `transfer` method under `Contract Method Selector` 5. Set `_to` to your destination wallet address. This is the address that will receive the tokens withdrawn from the safe. 6. Set `_value` to the amount to withdraw, for example `1000000000000000000`. 7. Click `Add transaction`. Now that you added the transaction to withdraw the tokens, you need to add the one to the auxiliary metadata contract. 8. Edit the `Enter Address or ENS Name` to contain the address of the auxiliary token. In Goerli this is `0x89d2c2C7853AD6015d50f39A615196e17f13Cf59`. 9. For ABI, use: ``` [{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":false,"internalType":"bytes","name":"metadata","type":"bytes"}],"name":"MetadataReceived","type":"event"},{"inputs":[{"internalType":"bytes","name":"metadata","type":"bytes"}],"name":"emitBytes","outputs":[],"stateMutability":"nonpayable","type":"function"}] ``` This is obtained by looking at https://goerli.etherscan.io/token/0x01812c3856442d697AFCa11E97dCad64522a8856#code 10. Method will be automatically selected to the only one available - `emitBytes`. 11. `metadata (bytes)` should be 32 hex-encoded bytes (i.e. 64 hex chars) that represents the TxOut public key. For example, put `0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f`. 12. Click `Add transaction`. 13. Now that everything is ready, click `Create Batch` and then `Send Batch`. Once the transaction goes through your wallet should contain the deposit and you should see a log message from the reserve-auditor: ``` 2022-06-21 21:11:05.933236816 UTC INFO Processing withdrawal from multi-sig tx: GnosisSafeWithdrawal { id: None, eth_tx_hash: "0x0e781edb7739aa88ad2ffb6a69aab46ff9e32dbd0f0c87e4006a176838b075d2", eth_block_number: 10892902, safe_addr: "0xeC018400FFe5Ad6E0B42Aa592Ee1CF6092972dEe", token_address: "0xeC76FbFD75481839e456C4cb2cd23cda813f19B1", amount: 1000000000000000000, mc_tx_out_public_key_hex: "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f00" }, mc.app: mc-reserve-auditor, mc.module: mc_reserve_auditor::gnosis::sync, mc.src: reserve-auditor/src/gnosis/sync.rs:170 ```