Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new smart wallet #6084

Merged
merged 5 commits into from
Sep 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ module.exports = {
'@jessie.js/no-nested-await': 'warn',
},
},
{
files: ['*.ts'],
rules: {
// TS has this covered and eslint gets it wrong
'no-undef': 'off',
},
},
{
// disable type-aware linting in HTML
files: ['*.html'],
Expand Down
2 changes: 1 addition & 1 deletion packages/ERTP/src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@
* MathHelpers used by this Issuer.
* @property {() => DisplayInfo} getDisplayInfo Give information to UI
* on how to display amounts for this issuer.
* @property {() => Purse} makeEmptyPurse Make an empty purse of this
* @property {() => Purse<K>} makeEmptyPurse Make an empty purse of this
* brand.
* @property {IssuerIsLive} isLive
* @property {IssuerGetAmountOf<K>} getAmountOf
Expand Down
1 change: 1 addition & 0 deletions packages/cosmic-swingset/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"@agoric/assert": "^0.4.0",
"@agoric/cosmos": "^0.30.0",
"@agoric/deploy-script-support": "^0.9.0",
"@agoric/internal": "^0.1.0",
"@agoric/nat": "^4.1.0",
"@agoric/store": "^0.7.2",
"@agoric/swing-store": "^0.7.0",
Expand Down
3 changes: 1 addition & 2 deletions packages/cosmic-swingset/src/block-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
import anylogger from 'anylogger';

import { assert, details as X } from '@agoric/assert';

import * as BRIDGE_ID from '@agoric/vats/src/bridge-ids.js';
import { BridgeId as BRIDGE_ID } from '@agoric/internal';

import * as ActionType from './action-types.js';
import { parseParams } from './params.js';
Expand Down
27 changes: 27 additions & 0 deletions packages/internal/src/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// @ts-check
/** @file
*
* Some of this config info may make more sense in a particular package. However
* due to the maxNodeModuleJsDepth hack and our general lax dependency graph,
* sometimes rational placements cause type resolution errors.
*
* So as a work-around some constants that need access from more than one package are placed here.
*/

/**
* Event source ids used by the bridge device.
*/
export const BridgeId = {
BANK: 'bank',
CORE: 'core',
DIBC: 'dibc',
STORAGE: 'storage',
PROVISION: 'provision',
WALLET: 'wallet',
};
harden(BridgeId);

export const WalletName = {
depositFacet: 'depositFacet',
};
harden(WalletName);
1 change: 1 addition & 0 deletions packages/internal/src/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @ts-check

export * from './utils.js';
export * from './config.js';
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.
102 changes: 102 additions & 0 deletions packages/smart-wallet/src/invitations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// @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').RemoteInvitationMakers} 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}`,
);
return E(makers)[invitationMakerName](...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