Skip to content

Commit

Permalink
feat: new Smart Wallet
Browse files Browse the repository at this point in the history
  • Loading branch information
turadg committed Sep 7, 2022
1 parent eb81837 commit 9b85ce3
Show file tree
Hide file tree
Showing 18 changed files with 1,791 additions and 7 deletions.
70 changes: 67 additions & 3 deletions packages/smart-wallet/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,69 @@
# Smart Wallet contract
# Smart Wallet

Future home of the Smart Wallet contract.
The `walletFactory` contract provisions and manages smart wallets.

For the legacy contract, see [@agoric/legacy-smart-wallet](../wallet/contract/README.md).
## Usage

There can be zero or one wallets per Cosmos address.

1. Generate an address (off-chain)
2. Provision an account using that address, which causes a Bank to get created
??? What happens if you try to provision again using the same address? It's a Cosmos level transaction; maybe that fails.
3. Create a Wallet using the Bank (it includes the implementation of Virtual Purses so when you getAmount it goes down to the Golang layer)
??? What happens if you try to create another wallet using that bank?

1 Address : 0/1 Bank
1 Address : 1 `myAddressNamesAdmin`
1 Bank : 0/1 Wallet

By design there's a 1:1 across all four.

`namesByAddress` and `board` are shared by everybody.

`myAddressNamesAdmin` is from the account you provision.

## Design

See the [Attackers Guide](src/AttackersGuide.md) for security requirements.

Product requirements:

- provision a wallet
- execute offers using the wallet
- deposit payments into the wallet's purses
- notification of state changes

Each of the above has to work over two channels:

- ocap for JS in vats holding object references (e.g. factory or wallet)
- Cosmos signed messages

Non-requirements:

- Multiple purses per brand ([#6126](https://github.com/Agoric/agoric-sdk/issues/6126)). When this is a requirement we'll need some way to specify in offer execution which purses to take funds from. For UX we shouldn't require that specificity unless there are multiple purses. When there are, lack of specifier could throw or we could have a "default" purse for each brand.

# Testing

There are no automated tests yet verifying the smart wallet running on chain. Here are procedures you can use instead.

## Notifiers

```
# tab 1 (chain)
cd packages/cosmic-swingset/
make scenario2-setup scenario2-run-chain
# starts bare chain, don’t need AMM
# tab 2 (client server)
cd packages/cosmic-swingset/
make scenario2-run-client
# confirm no errors in logs
# tab 3 (interactive)
agoric open --repl
# confirm in browser that `home.wallet` and `home.smartWallet` exist
agd query vstorage keys 'published.wallet'
# confirm it has a key like `published.wallet.agoric1nqxg4pye30n3trct0hf7dclcwfxz8au84hr3ht`
agoric follow :published.wallet.agoric1nqxg4pye30n3trct0hf7dclcwfxz8au84hr3ht
# confirm it has JSON data
```
5 changes: 4 additions & 1 deletion packages/smart-wallet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,12 @@
},
"devDependencies": {
"ava": "^4.3.1",
"@agoric/cosmic-swingset": "^0.37.0",
"@agoric/cosmic-proto": "^0.1.0",
"@agoric/inter-protocol": "^0.11.0",
"@agoric/swingset-vat": "^0.28.0",
"@agoric/wallet-backend": "^0.12.1",
"@agoric/vats": "^0.10.0",
"@agoric/zoe": "^0.24.0",
"@endo/captp": "^2.0.13"
},
Expand All @@ -27,7 +31,6 @@
"@agoric/notifier": "^0.4.0",
"@agoric/store": "^0.7.2",
"@agoric/vat-data": "^0.3.1",
"@agoric/vats": "^0.10.0",
"@endo/far": "^0.2.9"
},
"keywords": [],
Expand Down
49 changes: 49 additions & 0 deletions packages/smart-wallet/src/AttackersGuide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# An Attacker's guide to Smart Wallets

This is an incomplete list of potential weak points that an attacker might want to focus
on when looking for ways to violate the integrity of Smart Wallets. It's here to help
defenders, as "attacker's mindset" is a good way to focus attention for the defender. The
list should correspond pretty closely to the set of assurances that Smart Wallest aims
to support.

## Factory

This is the contract instance. It is responsible for,

- provisioning wallets
- passing them messages over the bridge
- maintaining state through upgrade

## Individual Wallet

The design assumes that assets pass only in these ways:

1. IN on the `deposit` facet
2. IN by proceeds of an offer (`getPayouts()`)
3. OUT by making an offer

## Types of attack

### Theft

The wallet instances rest on the ocap model

### Destruction

No matter what message the contract must not drop any assets into the void. Pay special attention to the time between an offer withdrawing payments from the wallet's purse(s) and the payouts being deposited (or it being refunded for wants not satisfied).

If the attacker could force a fatal error somewhere (perhaps in their own wallet) it would terminate the vat, which holds the factory and all the wallets. Are the offer processing states robust to termination? For example, what happens if you withdraw $10 from your purse to a payment for your offer and the vat dies before you take payouts?

### Denial of service

#### Resource exhaustion

The factory provides wallets and to do so much keep a hold of all wallets
produced. To mitigate, these shouldn't be held in RAM. By design they are in a
ScaleBigMapStore backed by disk.

The wallet object holds many types of state. The state must not grow monotonically over use or an attacker could grow the cost of holding the wallet in RAM to so much that it kills the vat.

### Deadlock

If an attacker can craft a message that leads some part of the code to deadlock or wait indefinitely, this could prevent use.
104 changes: 104 additions & 0 deletions packages/smart-wallet/src/invitations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// @ts-check
import { AmountMath } from '@agoric/ertp';
import { E } from '@endo/far';

/**
* Supports three cases
* 1. source is a contract (in which case this takes an Instance to look up in zoe)
* 2. the invitation is already in your Zoe "invitation" purse so we need to query it
* - use the find/query invitation by kvs thing
* 3. continuing invitation in which the offer result from a previous invitation had an `invitationMakers` property
*
* @typedef {ContractInvitationSpec | PurseInvitationSpec | ContinuingInvitationSpec} InvitationSpec
*/
/**
* @typedef {{
* source: 'contract',
* instance: Instance,
* publicInvitationMaker: string,
* invitationArgs?: any[],
* }} ContractInvitationSpec
* @typedef {{
* source: 'purse',
* instance: Instance,
* description: string,
* }} PurseInvitationSpec
* @typedef {{
* source: 'continuing',
* previousOffer: number,
* invitationMakerName: string,
* invitationArgs?: any[],
* }} ContinuingInvitationSpec
*/

/**
* @typedef {Pick<StandardInvitationDetails, 'description' | 'instance'>} InvitationsPurseQuery
*/

/**
*
* @param {ERef<ZoeService>} zoe
* @param {Brand<'set'>} invitationBrand
* @param {Purse<'set'>} invitationsPurse
* @param {(fromOfferId: number) => import('./types').InvitationMakers} getInvitationContinuation
*/
export const makeInvitationsHelper = (
zoe,
invitationBrand,
invitationsPurse,
getInvitationContinuation,
) => {
// TODO(6062) validate params with patterns
const invitationGetters = /** @type {const} */ ({
/** @type {(spec: ContractInvitationSpec) => Promise<Invitation>} */
contract(spec) {
const { instance, publicInvitationMaker, invitationArgs = [] } = spec;
const pf = E(zoe).getPublicFacet(instance);
return E(pf)[publicInvitationMaker](...invitationArgs);
},
/** @type {(spec: PurseInvitationSpec) => Promise<Invitation>} */
async purse(spec) {
const { instance, description } = spec;
assert(instance && description, 'missing instance or description');
/** @type {Amount<'set'>} */
const purseAmount = await E(invitationsPurse).getCurrentAmount();
const match = AmountMath.getValue(invitationBrand, purseAmount).find(
details =>
details.description === description && details.instance === instance,
);
assert(match, `no matching purse for ${{ instance, description }}`);
const toWithDraw = AmountMath.make(invitationBrand, harden([match]));
console.log('.... ', { toWithDraw });

return E(invitationsPurse).withdraw(toWithDraw);
},
/** @type {(spec: ContinuingInvitationSpec) => Promise<Invitation>} */
continuing(spec) {
console.log('making continuing invitation', spec);
const { previousOffer, invitationArgs = [], invitationMakerName } = spec;
const makers = getInvitationContinuation(previousOffer);
assert(
makers,
`invalid value stored for previous offer ${previousOffer}`,
);
const make = makers[invitationMakerName];
assert(make, `invalid maker name ${invitationMakerName}`);
return make(...invitationArgs);
},
});
/** @type {(spec: InvitationSpec) => ERef<Invitation>} */
const invitationFromSpec = spec => {
switch (spec.source) {
case 'contract':
return invitationGetters.contract(spec);
case 'purse':
return invitationGetters.purse(spec);
case 'continuing':
return invitationGetters.continuing(spec);
default:
throw new Error('unrecognize invitation source');
}
};
return invitationFromSpec;
};
harden(makeInvitationsHelper);
Loading

0 comments on commit 9b85ce3

Please sign in to comment.