-
Notifications
You must be signed in to change notification settings - Fork 212
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
18 changed files
with
1,791 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
Oops, something went wrong.