Skip to content

Commit

Permalink
Merge pull request #469 from LF-Decentralized-Trust-labs/zeto-erc20
Browse files Browse the repository at this point in the history
Zeto support for deposit and withdraw with an ERC20 contract
  • Loading branch information
jimthematrix authored Dec 19, 2024
2 parents e7b9733 + dee5b77 commit a06db21
Show file tree
Hide file tree
Showing 54 changed files with 2,587 additions and 741 deletions.
28 changes: 28 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -196,17 +196,45 @@ task copyZetoZKPFiles(type: Copy) {
// Likely if the range of circuits grows, a separate docker tag will be required for builds
// of Paladin that have a wider range of pre-built circuits included.
from fileTree('domains/zeto/zkp') {
// anon
include 'anon.zkey'
include 'anon-vkey.json'
include 'anon_js/anon.wasm'
include 'anon_batch.zkey'
include 'anon_batch-vkey.json'
include 'anon_batch_js/anon_batch.wasm'
// anon_enc
include 'anon_enc.zkey'
include 'anon_enc-vkey.json'
include 'anon_enc_js/anon_enc.wasm'
include 'anon_enc_batch.zkey'
include 'anon_enc_batch-vkey.json'
include 'anon_enc_batch_js/anon_enc_batch.wasm'
// anon_nullifier
include 'anon_nullifier.zkey'
include 'anon_nullifier-vkey.json'
include 'anon_nullifier_js/anon_nullifier.wasm'
include 'anon_nullifier_batch.zkey'
include 'anon_nullifier_batch-vkey.json'
include 'anon_nullifier_batch_js/anon_nullifier_batch.wasm'
// deposit
include 'check_hashes_value.zkey'
include 'check_hashes_value-vkey.json'
include 'check_hashes_value_js/check_hashes_value.wasm'
// withdraw
include 'check_inputs_outputs_value.zkey'
include 'check_inputs_outputs_value-vkey.json'
include 'check_inputs_outputs_value_js/check_inputs_outputs_value.wasm'
include 'check_inputs_outputs_value_batch.zkey'
include 'check_inputs_outputs_value_batch-vkey.json'
include 'check_inputs_outputs_value_batch_js/check_inputs_outputs_value_batch.wasm'
// withdraw_nullifier
include 'check_nullifier_value.zkey'
include 'check_nullifier_value-vkey.json'
include 'check_nullifier_value_js/check_nullifier_value.wasm'
include 'check_nullifier_value_batch.zkey'
include 'check_nullifier_value_batch-vkey.json'
include 'check_nullifier_value_batch_js/check_nullifier_value_batch.wasm'
}
into zetoZkpDir

Expand Down
52 changes: 52 additions & 0 deletions doc-site/docs/architecture/zeto.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,58 @@ Inputs:
- **to** - lookup string for the identity that will receive transferred value
- **amount** - amount of value to transfer

### deposit

The Zeto token implementations support interaction with an ERC20 token, to control the value supply publicly. With this paradigm, the token issuer, such as a central bank for digital currencies, can control the total supply in the ERC20 contract. This makes the supply of the tokens public.

The Zeto token contract can be configured to allow balances from a designated ERC20 contract to be "swapped" for Zeto tokens, by calling the `deposit` API. This allows any accounts that have a balance in the ERC20 contract to swap them for Zeto tokens. The exchange rate between the ERC20 and Zeto tokens is 1:1. On successful deposit, the ERC20 balance is transferred to the Zeto contract.

Typically in this paradigm, the `mint` API on the Zeto domain should be locked down (disabled) so that the only way to mint Zeto tokens is by depositing.

```json
{
"type": "function",
"name": "deposit",
"inputs": [
{
"name": "amount",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": null
}
```

Inputs:

- **amount** - amount of value to deposit

### withdraw

Opposite to the "deposit" operation, users can swap Zeto tokens back to ERC20 balances.

On successful withdrawal, the ERC20 balance is released by the Zeto contract and transferred back to the user account.

```json
{
"type": "function",
"name": "withdraw",
"inputs": [
{
"name": "amount",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": null
}
```

Inputs:

- **amount** - amount of value to withdraw

### lockProof

This is a special purpose function used in coordinating multi-party transactions, such as [Delivery-vs-Payment (DvP) contracts](https://github.com/hyperledger-labs/zeto/blob/main/solidity/contracts/zkDvP.sol). When a party commits to the trade first by uploading the ZK proof to the orchestration contract, they must be protected from a malicious party seeing the proof and using it to unilaterally execute the token transfer. The `lockProof()` function allows an account, which can be a smart contract address, to designate the finaly submitter of the proof, thus protecting anybody else from abusing the proof outside of the atomic settlement of the multi-leg trade.
Expand Down
91 changes: 86 additions & 5 deletions doc-site/docs/tutorials/zkp-cbdc.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
# CBDC Tokens based on Zeto
# Cash Tokens based on Zeto

The code for this tutorial can be found in [example/zeto](https://github.com/LF-Decentralized-Trust-labs/paladin/blob/main/example/zeto).

This shows how to leverage the [Zeto](../../architecture/zeto/) in order to build a wholesale CBDC with privacy, illustrating multiple aspects of Paladin's privacy capabilities.
This shows how to leverage the [Zeto](../../architecture/zeto/) in order to build a cash payment solution, for instance wholesale CBDC or a payment rail with commercial bank money, with privacy, illustrating multiple aspects of Paladin's privacy capabilities.

## Running the example

Follow the [Getting Started](../../getting-started/installation/) instructions to set up a Paldin environment, and
then follow the example [README](https://github.com/LF-Decentralized-Trust-labs/paladin/blob/main/example/zeto/README.md)
to run the code.

## Explanation
## Scenario #1: cash solution with private minting

In this scenario, the Zeto tokens are directly minted by the authority in the Zeto contract, making the mint amounts private. This also means the total supply of the Zeto tokens is unknown to the participants. Only the authority performing the minting operations is aware of the total supply.

Below is a walkthrough of each step in the example, with an explanation of what it does.

Expand All @@ -23,8 +25,7 @@ const zetoCBDC = await zetoFactory.newZeto(cbdcIssuer, {
});
```

This creates a new instance of the Zeto domain, using the [Zeto_AnonNullifier](https://github.com/hyperledger-labs/zeto/tree/main?tab=readme-ov-file#zeto_anonnullifier) contract.
This results in a new cloned contract on the base ledger, with a new unique address. This Zeto token contract will be used to represent
This creates a new instance of the Zeto domain, using the [Zeto_AnonNullifier](https://github.com/hyperledger-labs/zeto/tree/main?tab=readme-ov-file#zeto_anonnullifier) contract. This results in a new cloned contract on the base ledger, with a new unique address. This Zeto token contract will be used to represent
tokenized cash/CBDC.

The token will be minted by the central bank/CBDC issuer party. Minting is restricted to be requested only by the central bank, the
Expand Down Expand Up @@ -64,3 +65,83 @@ receipt = await zetoCBDC.using(paladin1).transfer(bank1, {

Bank1 can call the `transfer` function to transfer zeto tokens to multiple parties, up to 10. Note that the identity `bank1` exists on the `paladin1` instance,
therefore it must use that instance to send the transfer transction (`.using(paladin1)`).

## Scenario #2: cash solution with public minting

This scenario supports the requirement to make the total supply of the cash tokens public. This is achieved by making the authority perform the minting operations in an ERC20 contract. The participants can then exchange their ERC20 balances for Zeto tokens, by calling `deposit`, and exchange back to their ERC20 balances by calling `withdraw`.

Below is a walkthrough of each step in the example, with an explanation of what it does.

### Create CBDC token

```typescript
const zetoFactory = new ZetoFactory(paladin3, 'zeto');
const zetoCBDC = await zetoFactory.newZeto(cbdcIssuer, {
tokenName: 'Zeto_AnonNullifier',
});
```

This creates a new instance of the Zeto domain, using the [Zeto_AnonNullifier](https://github.com/hyperledger-labs/zeto/tree/main?tab=readme-ov-file#zeto_anonnullifier) contract. This results in a new cloned contract on the base ledger, with a new unique address. This Zeto token contract will be used to represent
tokenized cash/CBDC.

### Create public supply token (ERC20)

```typescript
const erc20Address = await deployERC20(paladin3, cbdcIssuer);
```

This deploys the ERC20 token which will be used by the authority to regulate the CBDC supply, with transparency to the paricipants.

### Configure the Zeto token contract to accept deposits and withdraws from the ERC20

```typescript
const result2 = await zetoCBDC.setERC20(cbdcIssuer, {
_erc20: erc20Address as string,
});
```

When the `deposit` function is called on the Zeto contract, this ERC20 contract will be called to draw the requested funds from the depositor's account. Conversely, when the `withdraw` function is called, this ERC20 contract will be called to transfer back the ERC20 balance to the withdrawer's account.

### Mint ERC20 tokens to publicly regulate CBDC supplies

```typescript
await mintERC20(paladin3, cbdcIssuer, bank1, erc20Address!, 100000);
```

Because the ERC20 implementation provides full transparency of the token operations, minting in the ERC20 allows all blockchain network participants to be aware of the overall supply of the CBDC tokens.

### Banks exchange ERC20 balances for Zeto tokens - deposit

```typescript
const result4 = await zetoCBDC.using(paladin1).deposit(bank1, {
amount: 10000,
});
```

After having been minted ERC20 balances, a partcipant like `bank1` can call `deposit` on the Paladin Zeto domain to exchange for Zeto tokens. Behind the scenes, the ERC20 balance is transferred to the Zeto contract which will hold until `withdraw` is called later.

### Bank1 transfers tokens to bank2 as payment

```typescript
receipt = await zetoCBDC.using(paladin1).transfer(bank1, {
transfers: [
{
to: bank2,
amount: 1000,
},
],
});
```

Bank1 can call the `transfer` function to transfer zeto tokens to multiple parties, up to 10. Note that the identity `bank1` exists on the `paladin1` instance,
therefore it must use that instance to send the transfer transction (`.using(paladin1)`).

### Bank1 exchanges Zeto tokens for ERC20 balances - withdraw

```typescript
const result5 = await zetoCBDC.using(paladin1).withdraw(bank1, {
amount: 1000,
});
```

A participant like `bank1` who has unspent Zeto tokens can call `withdraw` on the Paladin Zeto domain to exchange them for ERC20 balances. Behind the scenes, the requested amount are "burnt" in the Zeto contract, and the corresponding ERC20 amount are released by the Zeto contract, by transferring to the requesting account.
3 changes: 3 additions & 0 deletions domains/integration-test/zeto/config-for-deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ contracts:
- name: Zeto_Anon
verifier: Groth16Verifier_Anon
batchVerifier: Groth16Verifier_AnonBatch
depositVerifier: Groth16Verifier_CheckHashesValue
withdrawVerifier: Groth16Verifier_CheckInputsOutputsValue
batchWithdrawVerifier: Groth16Verifier_CheckInputsOutputsValueBatch
circuitId: anon
cloneable: true
abiAndBytecode:
Expand Down
17 changes: 2 additions & 15 deletions domains/noto/pkg/types/abi.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,30 +17,17 @@ package types

import (
_ "embed"
"encoding/json"

"github.com/hyperledger/firefly-signer/pkg/abi"
"github.com/kaleido-io/paladin/toolkit/pkg/pldapi"
"github.com/kaleido-io/paladin/toolkit/pkg/solutils"
"github.com/kaleido-io/paladin/toolkit/pkg/tktypes"
)

//go:embed abis/INotoPrivate.json
var notoPrivateJSON []byte

func mustParseBuildABI(buildJSON []byte) abi.ABI {
var buildParsed map[string]tktypes.RawJSON
var buildABI abi.ABI
err := json.Unmarshal(buildJSON, &buildParsed)
if err == nil {
err = json.Unmarshal(buildParsed["abi"], &buildABI)
}
if err != nil {
panic(err)
}
return buildABI
}

var NotoABI = mustParseBuildABI(notoPrivateJSON)
var NotoABI = solutils.MustParseBuildABI(notoPrivateJSON)

type ConstructorParams struct {
Notary string `json:"notary"` // Lookup string for the notary identity
Expand Down
1 change: 1 addition & 0 deletions domains/zeto/.gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
internal/zeto/abis/
pkg/types/abis/
integration-test/abis/
zkp/
tools/
Expand Down
35 changes: 23 additions & 12 deletions domains/zeto/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ ext {
goFilesE2E = fileTree(".") {
include "integration-test/**/*.go"
}
targetCoverage = 93.5
targetCoverage = 94.5
maxCoverageBarGap = 1
coveragePackages = [
"github.com/kaleido-io/paladin/domains/zeto/internal/...",
"github.com/kaleido-io/paladin/domains/zeto/pkg/types",
"github.com/kaleido-io/paladin/domains/zeto/pkg/zetosigner",
]

zetoVersion = "v0.0.7"
zetoVersion = "v0.0.10"
zetoHost = "hyperledger-labs"
zkpOut = "${projectDir}/zkp"
toolsOut = "${projectDir}/tools"
Expand Down Expand Up @@ -74,7 +74,7 @@ dependencies {
coreGo project(path: ":core:go", configuration: "goSource")
}

task downloadZetoProver {
task downloadZetoProver() {
def outname = "zeto-wasm-${zetoVersion}.tar.gz"
def url = "https://github.com/${zetoHost}/zeto/releases/download/${zetoVersion}/${outname}"
def f = new File(toolsOut, outname)
Expand All @@ -85,7 +85,7 @@ task downloadZetoProver {
outputs.file(f)
}

task downloadZetoTestProvingKeys {
task downloadZetoTestProvingKeys() {
def outname = "zeto-test-proving-keys-${zetoVersion}.tar.gz"
def url = "https://github.com/${zetoHost}/zeto/releases/download/${zetoVersion}/${outname}"
def f = new File(toolsOut, outname)
Expand All @@ -96,7 +96,7 @@ task downloadZetoTestProvingKeys {
outputs.file(f)
}

task downloadZetoCompiledContracts {
task downloadZetoCompiledContracts() {
def outname = "zeto-contracts-${zetoVersion}.tar.gz"
def url = "https://github.com/${zetoHost}/zeto/releases/download/${zetoVersion}/${outname}"
def f = new File(toolsOut, outname)
Expand Down Expand Up @@ -173,6 +173,19 @@ task copySolidity(type: Copy, dependsOn: [protoc, ":solidity:compile", extractZe
includeEmptyDirs = false
}

task copyPkgSolidity(type: Copy) {
inputs.files(configurations.contractCompile)

into 'pkg/types/abis'
from fileTree(configurations.contractCompile.asPath) {
include 'contracts/domains/interfaces/IZetoPrivate.sol/IZetoPrivate.json'
}

// Flatten all paths into the destination folder
eachFile { path = name }
includeEmptyDirs = false
}

task copySolidityForTest(type: Copy, dependsOn: [extractZetoArtifacts, generatePoseidonArtifacts, ":solidity:compile"]) {
inputs.files(configurations.contractCompile)
from fileTree(configurations.contractCompile.asPath) {
Expand All @@ -197,7 +210,7 @@ task copySolidityForTest(type: Copy, dependsOn: [extractZetoArtifacts, generateP
includeEmptyDirs = false
}

task testE2E(type: Exec, dependsOn: [protoc, copySolidity, copySolidityForTest, ':testinfra:startTestInfra', ":core:go:makeMocks"]) {
task testE2E(type: Exec, dependsOn: [protoc, copySolidity, copyPkgSolidity, copySolidityForTest, ':testinfra:startTestInfra', ":core:go:makeMocks"]) {
inputs.files(configurations.toolkitGo)
inputs.files(configurations.coreGo)
inputs.files(goFiles)
Expand All @@ -217,7 +230,7 @@ task testE2E(type: Exec, dependsOn: [protoc, copySolidity, copySolidityForTest,
helpers.dumpLogsOnFailure(it, ':testinfra:startTestInfra')
}

task unitTests(type: Exec, dependsOn: [protoc, ":core:go:makeMocks"]) {
task unitTests(type: Exec, dependsOn: [protoc, ":core:go:makeMocks", ":testinfra:startTestInfra", downloadZetoCompiledContracts, copySolidity, copyPkgSolidity]) {
inputs.files(configurations.toolkitGo)
inputs.files(configurations.coreGo)
inputs.files(goFiles)
Expand All @@ -238,9 +251,6 @@ task unitTests(type: Exec, dependsOn: [protoc, ":core:go:makeMocks"]) {
if (project.findProperty('verboseTests') == 'true') {
args '-v'
}

dependsOn copySolidity
dependsOn ':testinfra:startTestInfra'
helpers.dumpLogsOnFailure(it, ':testinfra:startTestInfra')
}

Expand All @@ -254,7 +264,7 @@ task test {
finalizedBy checkCoverage
}

task buildGo(type: GoLib, dependsOn: [":toolkit:go:protoc", copySolidity]) {
task buildGo(type: GoLib, dependsOn: [":toolkit:go:protoc", copySolidity, copyPkgSolidity]) {
inputs.files(configurations.coreGo)
inputs.files(configurations.toolkitGo)
baseName "zeto"
Expand All @@ -270,6 +280,7 @@ task build {
task clean(type: Delete) {
delete 'coverage'
delete 'internal/zeto/abis'
delete 'pkg/types/abis'
delete 'integration-test/abis'
delete zkpOut
delete toolsOut
Expand All @@ -280,7 +291,7 @@ task assemble {
}

dependencies {
goSource files(goFiles, copySolidity)
goSource files(goFiles, copySolidity, copyPkgSolidity)
zetoArtifacts files(extractZetoArtifacts)
poseidonArtifacts files(generatePoseidonArtifacts)
}
Loading

0 comments on commit a06db21

Please sign in to comment.