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

Dex happy path #484

Merged
merged 30 commits into from
Oct 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
a24aa91
Remove clone for lazyAuthorization
MartinMinkov Oct 13, 2022
0e5648e
Update dex example
MartinMinkov Oct 13, 2022
95ace78
Merge branch 'main' into dex/redeem-liquidity
mitschabaude Oct 14, 2022
ceb4350
fix dummy proof logic
mitschabaude Oct 14, 2022
7d2fd7e
fixes to dex contract logic & balance sum
mitschabaude Oct 14, 2022
b963051
minor
mitschabaude Oct 14, 2022
ae3b5d9
handle more cases when making account update a child
mitschabaude Oct 14, 2022
1f99103
try setting some zkapps to delegate_call, but creates different token…
mitschabaude Oct 14, 2022
3afba80
typos
mitschabaude Oct 14, 2022
0eebe07
make redeem liquidity work
mitschabaude Oct 14, 2022
ce8999b
minor
mitschabaude Oct 14, 2022
9f68d9d
enable changing proofsEnabled
mitschabaude Oct 17, 2022
15df826
dex debugging with proofs
mitschabaude Oct 17, 2022
bab4483
make Circuit.witness create a new unchecked snark context, and only c…
mitschabaude Oct 17, 2022
7fd540e
fix assertion about hashed child
mitschabaude Oct 17, 2022
a616c01
any children
mitschabaude Oct 17, 2022
cf41021
allow children which can't delegate token spending power, as default …
mitschabaude Oct 18, 2022
c417720
wip swap happy path
mitschabaude Oct 18, 2022
da6622d
missing access permission issue repro
mitschabaude Oct 18, 2022
8786c26
swap fix
mitschabaude Oct 18, 2022
30616d2
refine the logic for when `exists` is called
mitschabaude Oct 19, 2022
4a992ad
Circuit.log
mitschabaude Oct 19, 2022
f49f613
stop implicitly attaching account updates to tx
mitschabaude Oct 19, 2022
6a4c86f
remove debugging
mitschabaude Oct 19, 2022
853ce0b
remove unused z token
mitschabaude Oct 19, 2022
08f34b3
changelog
mitschabaude Oct 19, 2022
0b7940b
remove redundant checks guarding Circuit.witness
mitschabaude Oct 19, 2022
3fbc12c
fix precondition tests
mitschabaude Oct 19, 2022
8e6bc5e
changelog
mitschabaude Oct 19, 2022
0647981
fix attach logic
mitschabaude Oct 19, 2022
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
9 changes: 6 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added `VerificationKey`, which is a `Struct` with auxiliary data, to pass verification keys to a `@method`
- BREAKING CHANGE: Change names related to circuit types: `AsFieldsAndAux<T>` -> `Provable<T>`, `AsFieldElement<T>` -> `ProvablePure<T>`, `circuitValue` -> `provable`
- BREAKING CHANGE: Change all `ofFields` and `ofBits` methods on circuit types to `fromFields` and `fromBits`
- `SmartContract.experimental.authorize` to authorize a tree of child account updates https://github.com/o1-labs/snarkyjs/pull/428
- AccountUpdates are now valid `@method` arguments, and `authorize` is intended to be used on them when passed to a method
- Also replaces `Experimental.accountUpdateFromCallback`
- `SmartContract.experimental.authorize()` to authorize a tree of child account updates https://github.com/o1-labs/snarkyjs/pull/428
- AccountUpdates are now valid `@method` arguments, and `authorize()` is intended to be used on them when passed to a method
- Also replaces `Experimental.accountUpdateFromCallback()`
- `Circuit.log()` to easily log Fields and other provable types inside a method, with the same API as `console.log()`
- `AccountUpdate.attachToTransaction()` for explicitly adding an account update to the current transaction. This replaces some previous behaviour where an account update got attached implicitly.

### Changed

- BREAKING CHANGE: `tx.send()` is now asynchronous: old: `send(): TransactionId` new: `send(): Promise<TransactionId>` and `tx.send()` now directly waits for the network response, as opposed to `tx.send().wait()`
- `Circuit.witness` can now be called outside circuits, where it will just directly return the callback result

### Deprecated

Expand Down
65 changes: 65 additions & 0 deletions src/examples/zkapps/dex/arbitrary_token_interaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {
isReady,
Mina,
AccountUpdate,
UInt64,
shutdown,
Token,
} from 'snarkyjs';
import { TokenContract, addresses, keys, tokenIds } from './dex.js';

await isReady;
let doProofs = true;

let Local = Mina.LocalBlockchain({ proofsEnabled: doProofs });
Mina.setActiveInstance(Local);
let accountFee = Mina.accountCreationFee();

let [{ privateKey: userKey, publicKey: userAddress }] = Local.testAccounts;
let tx;

console.log('-------------------------------------------------');
console.log('TOKEN X ADDRESS\t', addresses.tokenX.toBase58());
console.log('USER ADDRESS\t', userAddress.toBase58());
console.log('-------------------------------------------------');
console.log('TOKEN X ID\t', Token.Id.toBase58(tokenIds.X));
console.log('-------------------------------------------------');

// compile & deploy all 5 zkApps
console.log('compile (token)...');
await TokenContract.compile();

let tokenX = new TokenContract(addresses.tokenX);

console.log('deploy & init token contracts...');
tx = await Mina.transaction(userKey, () => {
// pay fees for creating 2 token contract accounts, and fund them so each can create 1 account themselves
let feePayerUpdate = AccountUpdate.createSigned(userKey);
feePayerUpdate.balance.subInPlace(accountFee.mul(1));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason for multiplying it times one? Probably not, just curious tho

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no there isn't :D it was copied from another place where it was multiplied by 2 I guess

tokenX.deploy();
});
await tx.prove();
tx.sign([keys.tokenX]);
await tx.send();

console.log('arbitrary token minting...');
tx = await Mina.transaction(userKey, () => {
// pay fees for creating user's token X account
AccountUpdate.createSigned(userKey).balance.subInPlace(accountFee.mul(1));
// 😈😈😈 mint any number of tokens to our account 😈😈😈
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love the comment emojis ha!

let tokenContract = new TokenContract(addresses.tokenX);
tokenContract.experimental.token.mint({
address: userAddress,
amount: UInt64.from(1e18),
});
});
await tx.prove();
console.log(tx.toPretty());
await tx.send();

console.log(
'User tokens: ',
Mina.getBalance(userAddress, tokenIds.X).value.toBigInt()
);

shutdown();
138 changes: 85 additions & 53 deletions src/examples/zkapps/dex/dex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
Bool,
Circuit,
DeployArgs,
Experimental,
Field,
Int64,
isReady,
Expand All @@ -18,15 +17,26 @@ import {
UInt64,
VerificationKey,
Struct,
State,
state,
} from 'snarkyjs';

export { Dex, DexTokenHolder, TokenContract, keys, addresses, tokenIds };

class UInt64x2 extends Struct([UInt64, UInt64]) {}

class Dex extends SmartContract {
// addresses of token contracts are constants
tokenX = addresses.tokenX;
tokenY = addresses.tokenY;

/**
* state which keeps track of total lqXY supply -- this is needed to calculate what to return when redeeming liquidity
*
* total supply is zero initially; it increases when supplying liquidity and decreases when redeeming it
*/
@state(UInt64) totalSupply = State<UInt64>();

/**
* Mint liquidity tokens in exchange for X and Y tokens
* @param user caller address
Expand All @@ -40,11 +50,7 @@ class Dex extends SmartContract {
*
* The transaction needs to be signed by the user's private key.
*/
@method supplyLiquidityBase(
user: PublicKey,
dx: UInt64,
dy: UInt64
) /*: UInt64 */ {
@method supplyLiquidityBase(user: PublicKey, dx: UInt64, dy: UInt64): UInt64 {
let tokenX = new TokenContract(this.tokenX);
let tokenY = new TokenContract(this.tokenY);

Expand All @@ -58,6 +64,7 @@ class Dex extends SmartContract {
let isXZero = dexXBalance.equals(UInt64.zero);
let xSafe = Circuit.if(isXZero, UInt64.one, dexXBalance);

// FIXME
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this important?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes! reminds us to fix it

// Error: Constraint unsatisfied (unreduced): Equal 0 1
// dy.equals(dx.mul(dexYBalance).div(xSafe)).or(isXZero).assertTrue();

Expand All @@ -68,7 +75,12 @@ class Dex extends SmartContract {
// // => maintains ratio x/l, y/l
let dl = dy.add(dx);
this.experimental.token.mint({ address: user, amount: dl });
// return dl;

// update l supply
let l = this.totalSupply.get();
this.totalSupply.assertEquals(l);
this.totalSupply.set(l.add(dl));
return dl;
}

/**
Expand All @@ -83,7 +95,7 @@ class Dex extends SmartContract {
*
* The transaction needs to be signed by the user's private key.
*/
supplyLiquidity(user: PublicKey, dx: UInt64) /*: UInt64 */ {
supplyLiquidity(user: PublicKey, dx: UInt64): UInt64 {
// calculate dy outside circuit
let x = Account(this.address, Token.getId(this.tokenX)).balance.get();
let y = Account(this.address, Token.getId(this.tokenY)).balance.get();
Expand All @@ -93,8 +105,7 @@ class Dex extends SmartContract {
);
}
let dy = dx.mul(y).div(x);
this.supplyLiquidityBase(user, dx, dy);
// return this.supplyLiquidityBase(user, dx, dy);
return this.supplyLiquidityBase(user, dx, dy);
}

/**
Expand All @@ -105,12 +116,13 @@ class Dex extends SmartContract {
*
* The transaction needs to be signed by the user's private key.
*/
@method redeemLiquidity(user: PublicKey, dl: UInt64): UInt64x2 {
@method redeemLiquidity(user: PublicKey, dl: UInt64) {
// call the token X holder inside a token X-authorized callback
let tokenX = new TokenContract(this.tokenX);
let dexX = new DexTokenHolder(this.address, tokenX.experimental.token.id);
let dxdy = dexX.redeemLiquidity(user, dl, this.tokenY);
tokenX.authorizeUpdate(dexX.self);
let dx = dxdy[0];
tokenX.authorizeUpdateAndSend(dexX.self, user, dx);
return dxdy;
}

Expand All @@ -126,7 +138,7 @@ class Dex extends SmartContract {
let tokenY = new TokenContract(this.tokenY);
let dexY = new DexTokenHolder(this.address, tokenY.experimental.token.id);
let dy = dexY.swap(user, dx, this.tokenX);
tokenY.authorizeUpdate(dexY.self);
tokenY.authorizeUpdateAndSend(dexY.self, user, dy);
return dy;
}

Expand All @@ -142,33 +154,51 @@ class Dex extends SmartContract {
let tokenX = new TokenContract(this.tokenX);
let dexX = new DexTokenHolder(this.address, tokenX.experimental.token.id);
let dx = dexX.swap(user, dy, this.tokenY);
tokenX.authorizeUpdate(dexX.self);
tokenX.authorizeUpdateAndSend(dexX.self, user, dx);
return dx;
}
}

class UInt64x2 extends Struct([UInt64, UInt64]) {}
/**
* helper method to authorize burning of user's liquidity.
* this just burns user tokens, so there is no incentive to call this directly.
* instead, the dex token holders call this and in turn pay back tokens.
*
* @param user caller address
* @param dl input amount of lq tokens
* @returns total supply of lq tokens _before_ burning dl, so that caller can calculate how much dx / dx to returns
*
* The transaction needs to be signed by the user's private key.
*/
@method burnLiquidity(user: PublicKey, dl: UInt64): UInt64 {
// this makes sure there is enough l to burn (user balance stays >= 0), so l stays >= 0, so l was >0 before
this.experimental.token.burn({ address: user, amount: dl });
let l = this.totalSupply.get();
this.totalSupply.assertEquals(l);
this.totalSupply.set(l.sub(dl));
return l;
}
}

class DexTokenHolder extends SmartContract {
// simpler circuit for redeeming liquidity -- direct trade between our token and lq token
// it's incomplete, as it gives the user only the Y part for an lqXY token; but doesn't matter as there's no incentive to call it directly
// see the more complicated method `redeemLiquidity` below which gives back both tokens, by calling this method,
// for the other token, in a callback
@method redeemLiquidityPartial(user: PublicKey, dl: UInt64): UInt64x2 {
let dex = AccountUpdate.create(this.address);
let l = dex.account.balance.get();
dex.account.balance.assertEquals(l);

// user sends dl to dex
let idlXY = Token.getId(this.address);
let userUpdate = AccountUpdate.create(user, idlXY);
userUpdate.balance.subInPlace(dl);
// user burns dl, authorized by the Dex main contract
let dex = new Dex(addresses.dex);
let l = dex.burnLiquidity(user, dl);

// in return, we give dy back
let y = this.account.balance.get();
this.account.balance.assertEquals(y);
// we can safely divide by l here because the Dex contract logic wouldn't allow burnLiquidity if not l>0
let dy = y.mul(dl).div(l);
this.send({ to: user, amount: dy });
// just subtract the balance, user gets their part one level higher
this.balance.subInPlace(dy);

// this can't be a delegate call, or it won't be authorized by the token owner
this.self.isDelegateCall = Bool(false);

// return l, dy so callers don't have to walk their child account updates to get it
return [l, dy];
Expand All @@ -186,13 +216,17 @@ class DexTokenHolder extends SmartContract {
let result = dexY.redeemLiquidityPartial(user, dl);
let l = result[0];
let dy = result[1];
tokenY.authorizeUpdate(dexY.self);
tokenY.authorizeUpdateAndSend(dexY.self, user, dy);

// in return for dl, we give back dx, the X token part
let x = this.account.balance.get();
this.account.balance.assertEquals(x);
let dx = x.mul(dl).div(l);
this.send({ to: user, amount: dx });
// just subtract the balance, user gets their part one level higher
this.balance.subInPlace(dx);

// this can't be a delegate call, or it won't be authorized by the token owner
this.self.isDelegateCall = Bool(false);

return [dx, dy];
}
Expand All @@ -206,20 +240,18 @@ class DexTokenHolder extends SmartContract {
// we're writing this as if our token == y and other token == x
let dx = otherTokenAmount;
let tokenX = new TokenContract(otherTokenAddress);
// send x from user to us (i.e., to the same address as this but with the other token)
let dexX = tokenX.experimental.token.send({
from: user,
to: this.address,
amount: dx,
});
// get balances
let x = dexX.account.balance.get();
dexX.account.balance.assertEquals(x);
let x = tokenX.getBalance(this.address);
let y = this.account.balance.get();
this.account.balance.assertEquals(y);
// send x from user to us (i.e., to the same address as this but with the other token)
tokenX.transfer(user, this.address, dx);
// compute and send dy
let dy = y.mul(dx).div(x.add(dx));
this.send({ to: user, amount: dy });
// just subtract dy balance and let adding balance be handled one level higher
this.balance.subInPlace(dy);
// not be a delegate call
this.self.isDelegateCall = Bool(false);
return dy;
}
}
Expand All @@ -235,7 +267,8 @@ class TokenContract extends SmartContract {
super.deploy(args);
this.setPermissions({
...Permissions.default(),
send: Permissions.proofOrSignature(),
send: Permissions.proof(),
receive: Permissions.proof(),
});
}
@method init() {
Expand Down Expand Up @@ -265,21 +298,19 @@ class TokenContract extends SmartContract {
zkapp.sign();
}

// let a zkapp do whatever it wants, as long as the token supply stays constant
@method authorizeUpdate(zkappUpdate: AccountUpdate) {
// adopt this account update as a child, allowing a certain layout for its own children
// we allow 10 child account updates, in a left-biased tree of width 3
let { NoChildren, StaticChildren } = AccountUpdate.Layout;
let layout = StaticChildren(
StaticChildren(StaticChildren(3), NoChildren, NoChildren),
NoChildren,
NoChildren
);
this.experimental.authorize(zkappUpdate, layout);

// walk account updates to see if balances for this token cancel
let balance = balanceSum(zkappUpdate, this.experimental.token.id);
balance.assertEquals(Int64.zero);
// let a zkapp send tokens to someone, provided the token supply stays constant
@method authorizeUpdateAndSend(
zkappUpdate: AccountUpdate,
to: PublicKey,
amount: UInt64
) {
this.experimental.authorize(zkappUpdate);

// see if balance change cancels the amount sent
let balanceChange = Int64.fromObject(zkappUpdate.body.balanceChange);
balanceChange.assertEquals(Int64.from(amount).neg());
// add same amount of tokens to the receiving address
this.experimental.token.mint({ address: to, amount });
}

@method transfer(from: PublicKey, to: PublicKey, value: UInt64) {
Expand Down Expand Up @@ -315,7 +346,7 @@ function balanceSum(accountUpdate: AccountUpdate, tokenId: Field) {
let myBalance = Int64.fromObject(accountUpdate.body.balanceChange);
let balance = Circuit.if(myTokenId.equals(tokenId), myBalance, Int64.zero);
for (let child of accountUpdate.children.accountUpdates) {
balance.add(balanceSum(child, tokenId));
balance = balance.add(balanceSum(child, tokenId));
}
return balance;
}
Expand All @@ -331,6 +362,7 @@ function randomAccounts<K extends string>(
'EKFE2UKugtoVMnGTxTakF2M9wwL9sp4zrxSLhuzSn32ZAYuiKh5R',
'EKEn2s1jSNADuC8CmvCQP5CYMSSoNtx5o65H7Lahqkqp2AVdsd12',
'EKE21kTAb37bekHbLvQpz2kvDYeKG4hB21x8VTQCbhy6m2BjFuxA',
'EKF9JA8WiEAk7o3ENnvgMHg5XKwgQfyMowNFFrEDCevoSozSgLTn',
];

let keys = Object.fromEntries(
Expand Down
Loading