Skip to content

Commit ca3919e

Browse files
authored
Merge pull request #1 from 10d9e/10d9e/discrete-erc20-wrapper
added DiscreteBridge to enable bridging between regular ERC20s and Di…
2 parents e464906 + c9c9585 commit ca3919e

20 files changed

+4564
-471
lines changed

.gitignore

+3-1
Original file line numberDiff line numberDiff line change
@@ -147,4 +147,6 @@ node_modules
147147
# Hardhat Ignition default folder for deployments against a local node
148148
ignition/deployments/chain-31337
149149

150-
.vscode
150+
.vscode
151+
152+
ignition/deployments

bun.lock

+1,732
Large diffs are not rendered by default.

contracts/examples/DiscreteERC20.sol contracts/DiscreteERC20.sol

+29-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// SPDX-License-Identifier: MIT
22
pragma solidity ^0.8.24;
33

4-
import "../Paillier.sol";
4+
import "./Paillier.sol";
55

66
// Secp256k1Ciphertext struct
77
struct Secp256k1Ciphertext {
@@ -81,6 +81,10 @@ contract DiscreteERC20 {
8181
totalSupply = _initialSupply;
8282
}
8383

84+
function getBalance(address account) external view returns (Ciphertext memory) {
85+
return balanceOf[account];
86+
}
87+
8488
/// @notice Emits an event to request the balance of the sender
8589
function requestBalance() external {
8690
emit RequestBalance(msg.sender);
@@ -113,6 +117,30 @@ contract DiscreteERC20 {
113117
return true;
114118
}
115119

120+
/// @notice Transfers encrypted tokens to a specified address
121+
/// @param recipient The address to receive the tokens
122+
/// @param amount The amount of tokens to transfer, represented as encrypted data
123+
/// @return success A boolean value indicating success of the transfer
124+
function transfer_proxy(
125+
address sender,
126+
address recipient,
127+
Ciphertext calldata amount
128+
) external returns (bool success) {
129+
require(
130+
keccak256(abi.encodePacked(balanceOf[sender].value)) != keccak256(bytes("")),
131+
"DiscreteERC20: transfer from the zero address"
132+
);
133+
134+
if (keccak256(abi.encodePacked(balanceOf[recipient].value)) == keccak256(bytes(""))) {
135+
balanceOf[recipient] = amount;
136+
} else {
137+
balanceOf[recipient] = this._add(balanceOf[recipient], amount);
138+
}
139+
balanceOf[sender] = this._sub(balanceOf[sender], amount);
140+
emit Transfer(sender, recipient, amount);
141+
return true;
142+
}
143+
116144
/// @dev Internal function to mint new encrypted tokens
117145
/// @param to The address to receive the newly minted tokens
118146
/// @param amount The amount of tokens to mint, represented as encrypted data

contracts/DiscreteERC20Bridge.sol

+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.24;
3+
4+
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5+
import "./DiscreteERC20.sol";
6+
import "./Paillier.sol";
7+
import "./PaillierProofVerifier.sol";
8+
9+
/// @title DiscreteERC20Bridge
10+
/// @dev Wraps a standard ERC20 token, enabling encrypted transactions through the DiscreteERC20 contract.
11+
contract DiscreteERC20Bridge {
12+
IERC20 public immutable underlyingToken;
13+
DiscreteERC20 public discreteToken;
14+
Paillier private immutable paillier;
15+
PublicKey public publicKey;
16+
Groth16Verifier public verifier; // zk-SNARK verifier contract
17+
18+
/// @notice Event emitted when a user deposits ERC20 tokens to mint encrypted tokens.
19+
event Deposit(address indexed user, uint256 amount, Ciphertext encryptedAmount);
20+
21+
/// @notice Event emitted when a user withdraws by burning encrypted tokens.
22+
event Withdraw(address indexed user, uint256 amount);
23+
24+
/// @notice Event emitted when a user transfers encrypted tokens.
25+
event TransferEncrypted(address indexed from, address indexed to, Ciphertext amount);
26+
27+
/// @param _underlyingToken Address of the standard ERC20 token.
28+
/// @param _discreteToken Address of the DiscreteERC20 contract.
29+
/// @param _paillier Address of the Paillier contract.
30+
/// @param _publicKey Public key for Paillier encryption.
31+
constructor(
32+
address _underlyingToken,
33+
address _discreteToken,
34+
address _paillier,
35+
PublicKey memory _publicKey,
36+
address _verifier
37+
) {
38+
underlyingToken = IERC20(_underlyingToken);
39+
discreteToken = DiscreteERC20(_discreteToken);
40+
paillier = Paillier(_paillier);
41+
publicKey = _publicKey;
42+
verifier = Groth16Verifier(_verifier);
43+
}
44+
45+
function verifyProof(
46+
uint256[2] calldata /* a */,
47+
uint256[2][2] calldata /*b */,
48+
uint256[2] calldata /*c */,
49+
uint256[3] calldata /*input */
50+
) public pure returns (bool) {
51+
// todo: integrate verifier
52+
return true;
53+
// return verifier.verifyProof(a, b, c, input);
54+
}
55+
56+
/// @notice Deposit ERC20 tokens and mint encrypted tokens.
57+
/// @param amount The amount of ERC20 tokens to deposit.
58+
function deposit(uint256 amount) external {
59+
require(amount > 0, "Cannot deposit zero tokens");
60+
require(underlyingToken.transferFrom(msg.sender, address(this), amount), "Transfer failed");
61+
62+
bytes memory randomValue = _generateRandomValue();
63+
BigNumber memory encryptedAmount = paillier.encrypt(amount, randomValue, publicKey);
64+
Ciphertext memory ciphertext = Ciphertext(encryptedAmount.val);
65+
66+
discreteToken.mint(msg.sender, ciphertext);
67+
emit Deposit(msg.sender, amount, ciphertext);
68+
}
69+
70+
/// @notice Transfer encrypted tokens between users.
71+
/// @param to The recipient address.
72+
/// @param encryptedAmount The encrypted amount to transfer.
73+
function transferEncrypted(address to, Ciphertext calldata encryptedAmount) external {
74+
discreteToken.transfer_proxy(msg.sender, to, encryptedAmount);
75+
emit TransferEncrypted(msg.sender, to, encryptedAmount);
76+
}
77+
78+
/// @notice Withdraw ERC20 tokens by burning encrypted tokens.
79+
/// @param amount The amount to withdraw.
80+
function withdraw(
81+
uint256 amount,
82+
uint256[2] calldata pok_a,
83+
uint256[2][2] calldata pok_b,
84+
uint256[2] calldata pok_c,
85+
uint256[3] calldata pok_input
86+
) external {
87+
require(verifyProof(pok_a, pok_b, pok_c, pok_input), "zk-pok invalid");
88+
require(amount > 0, "Invalid amount");
89+
require(underlyingToken.transfer(msg.sender, amount), "Transfer failed");
90+
91+
bytes memory randomValue = _generateRandomValue();
92+
BigNumber memory encryptedAmount = paillier.encrypt(amount, randomValue, publicKey);
93+
Ciphertext memory ciphertext = Ciphertext(encryptedAmount.val);
94+
discreteToken.burn(msg.sender, ciphertext);
95+
96+
//discreteToken.burn(msg.sender, discreteToken.getBalance(msg.sender));
97+
emit Withdraw(msg.sender, amount);
98+
}
99+
100+
/// @dev Generates a random value for encryption.
101+
function _generateRandomValue() internal view returns (bytes memory) {
102+
return abi.encodePacked(block.timestamp, msg.sender);
103+
}
104+
105+
/// @dev Converts a BigNumber to uint256.
106+
function _bigNumberToUint256(BigNumber memory bn) internal pure returns (uint256) {
107+
return abi.decode(bn.val, (uint256));
108+
}
109+
110+
/// @dev Returns the private key (mocked, should be handled off-chain).
111+
function _getPrivateKey() internal pure returns (PrivateKey memory) {
112+
return PrivateKey("", "");
113+
}
114+
}

contracts/ERC20Mock.sol

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.24;
3+
4+
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
5+
6+
contract ERC20Mock is ERC20 {
7+
constructor(string memory name, string memory symbol, uint8 decimals) ERC20(name, symbol) {
8+
_mint(msg.sender, 1_000_000 * (10 ** uint256(decimals))); // Mint initial supply
9+
}
10+
11+
function mint(address to, uint256 amount) external {
12+
_mint(to, amount);
13+
}
14+
}

contracts/PaillierProofVerifier.sol

+182
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
// SPDX-License-Identifier: GPL-3.0
2+
/*
3+
Copyright 2021 0KIMS association.
4+
5+
This file is generated with [snarkJS](https://github.com/iden3/snarkjs).
6+
7+
snarkJS is a free software: you can redistribute it and/or modify it
8+
under the terms of the GNU General Public License as published by
9+
the Free Software Foundation, either version 3 of the License, or
10+
(at your option) any later version.
11+
12+
snarkJS is distributed in the hope that it will be useful, but WITHOUT
13+
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14+
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
15+
License for more details.
16+
17+
You should have received a copy of the GNU General Public License
18+
along with snarkJS. If not, see <https://www.gnu.org/licenses/>.
19+
*/
20+
21+
pragma solidity >=0.7.0 <0.9.0;
22+
23+
contract Groth16Verifier {
24+
// Scalar field size
25+
uint256 constant r = 21888242871839275222246405745257275088548364400416034343698204186575808495617;
26+
// Base field size
27+
uint256 constant q = 21888242871839275222246405745257275088696311157297823662689037894645226208583;
28+
29+
// Verification Key data
30+
uint256 constant alphax = 5626737339165458973589599457621328529733563392239816845745153285393308563504;
31+
uint256 constant alphay = 16230467856761692471715032392328021447033402595640524796664100872627369680070;
32+
uint256 constant betax1 = 4144428083991626285342649939274849449280404441912827482121791540345299786531;
33+
uint256 constant betax2 = 6325433196787304365631895536258959371015671431667950220195827813497107168985;
34+
uint256 constant betay1 = 10936661144817015255674518731163444456215109280399110754173599934895271139411;
35+
uint256 constant betay2 = 19889497587240439239166766822075868131117808253130075878786112720929398512804;
36+
uint256 constant gammax1 = 11559732032986387107991004021392285783925812861821192530917403151452391805634;
37+
uint256 constant gammax2 = 10857046999023057135944570762232829481370756359578518086990519993285655852781;
38+
uint256 constant gammay1 = 4082367875863433681332203403145435568316851327593401208105741076214120093531;
39+
uint256 constant gammay2 = 8495653923123431417604973247489272438418190587263600148770280649306958101930;
40+
uint256 constant deltax1 = 17295485814694150123783000481296321702245813170311037729305904660722399839036;
41+
uint256 constant deltax2 = 9953038103920179753358196237980982556490877057040907085330335881158104037356;
42+
uint256 constant deltay1 = 6539691989876630411606817978414648636843419931042291769251371916370543524601;
43+
uint256 constant deltay2 = 13144314962000242455271345157960851628353417380470981436942002942254462986698;
44+
45+
46+
uint256 constant IC0x = 18387697014443789242231700969179111271118402915177266877757196673698635297519;
47+
uint256 constant IC0y = 12436236940538570506589398884575585672721049376613458579755675085941516075140;
48+
49+
uint256 constant IC1x = 4524932351881239948215022805183121776177868798590915616981221083576167527145;
50+
uint256 constant IC1y = 15587504486709804185620332672789255104510724034018287661184516721901064633674;
51+
52+
uint256 constant IC2x = 14439831487577900683535901946347940295203534376602237241423095901557646925805;
53+
uint256 constant IC2y = 10717629512118312536692599889074200105443234230892842440615136924549793591272;
54+
55+
uint256 constant IC3x = 10024533475695360204705392683236727525700242453221038868275481275771880537493;
56+
uint256 constant IC3y = 4765705996809486869329645343856463186100886171625349210938486327289623735490;
57+
58+
59+
// Memory data
60+
uint16 constant pVk = 0;
61+
uint16 constant pPairing = 128;
62+
63+
uint16 constant pLastMem = 896;
64+
65+
function verifyProof(uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[3] calldata _pubSignals) public view returns (bool) {
66+
assembly {
67+
function checkField(v) {
68+
if iszero(lt(v, r)) {
69+
mstore(0, 0)
70+
return(0, 0x20)
71+
}
72+
}
73+
74+
// G1 function to multiply a G1 value(x,y) to value in an address
75+
function g1_mulAccC(pR, x, y, s) {
76+
let success
77+
let mIn := mload(0x40)
78+
mstore(mIn, x)
79+
mstore(add(mIn, 32), y)
80+
mstore(add(mIn, 64), s)
81+
82+
success := staticcall(sub(gas(), 2000), 7, mIn, 96, mIn, 64)
83+
84+
if iszero(success) {
85+
mstore(0, 0)
86+
return(0, 0x20)
87+
}
88+
89+
mstore(add(mIn, 64), mload(pR))
90+
mstore(add(mIn, 96), mload(add(pR, 32)))
91+
92+
success := staticcall(sub(gas(), 2000), 6, mIn, 128, pR, 64)
93+
94+
if iszero(success) {
95+
mstore(0, 0)
96+
return(0, 0x20)
97+
}
98+
}
99+
100+
function checkPairing(pA, pB, pC, pubSignals, pMem) -> isOk {
101+
let _pPairing := add(pMem, pPairing)
102+
let _pVk := add(pMem, pVk)
103+
104+
mstore(_pVk, IC0x)
105+
mstore(add(_pVk, 32), IC0y)
106+
107+
// Compute the linear combination vk_x
108+
109+
g1_mulAccC(_pVk, IC1x, IC1y, calldataload(add(pubSignals, 0)))
110+
111+
g1_mulAccC(_pVk, IC2x, IC2y, calldataload(add(pubSignals, 32)))
112+
113+
g1_mulAccC(_pVk, IC3x, IC3y, calldataload(add(pubSignals, 64)))
114+
115+
116+
// -A
117+
mstore(_pPairing, calldataload(pA))
118+
mstore(add(_pPairing, 32), mod(sub(q, calldataload(add(pA, 32))), q))
119+
120+
// B
121+
mstore(add(_pPairing, 64), calldataload(pB))
122+
mstore(add(_pPairing, 96), calldataload(add(pB, 32)))
123+
mstore(add(_pPairing, 128), calldataload(add(pB, 64)))
124+
mstore(add(_pPairing, 160), calldataload(add(pB, 96)))
125+
126+
// alpha1
127+
mstore(add(_pPairing, 192), alphax)
128+
mstore(add(_pPairing, 224), alphay)
129+
130+
// beta2
131+
mstore(add(_pPairing, 256), betax1)
132+
mstore(add(_pPairing, 288), betax2)
133+
mstore(add(_pPairing, 320), betay1)
134+
mstore(add(_pPairing, 352), betay2)
135+
136+
// vk_x
137+
mstore(add(_pPairing, 384), mload(add(pMem, pVk)))
138+
mstore(add(_pPairing, 416), mload(add(pMem, add(pVk, 32))))
139+
140+
141+
// gamma2
142+
mstore(add(_pPairing, 448), gammax1)
143+
mstore(add(_pPairing, 480), gammax2)
144+
mstore(add(_pPairing, 512), gammay1)
145+
mstore(add(_pPairing, 544), gammay2)
146+
147+
// C
148+
mstore(add(_pPairing, 576), calldataload(pC))
149+
mstore(add(_pPairing, 608), calldataload(add(pC, 32)))
150+
151+
// delta2
152+
mstore(add(_pPairing, 640), deltax1)
153+
mstore(add(_pPairing, 672), deltax2)
154+
mstore(add(_pPairing, 704), deltay1)
155+
mstore(add(_pPairing, 736), deltay2)
156+
157+
158+
let success := staticcall(sub(gas(), 2000), 8, _pPairing, 768, _pPairing, 0x20)
159+
160+
isOk := and(success, mload(_pPairing))
161+
}
162+
163+
let pMem := mload(0x40)
164+
mstore(0x40, add(pMem, pLastMem))
165+
166+
// Validate that all evaluations ∈ F
167+
168+
checkField(calldataload(add(_pubSignals, 0)))
169+
170+
checkField(calldataload(add(_pubSignals, 32)))
171+
172+
checkField(calldataload(add(_pubSignals, 64)))
173+
174+
175+
// Validate all evaluations
176+
let isValid := checkPairing(_pA, _pB, _pC, _pubSignals, pMem)
177+
178+
mstore(0, isValid)
179+
return(0, 0x20)
180+
}
181+
}
182+
}

hardhat.config.ts

+9
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import '@nomicfoundation/hardhat-ethers';
22
import '@nomicfoundation/hardhat-toolbox';
33
import { HardhatUserConfig } from 'hardhat/config';
44

5+
//import 'hardhat-circom';
6+
57
require('@nomicfoundation/hardhat-chai-matchers');
68

79
const mnemonic: string =
@@ -16,7 +18,14 @@ const config: HardhatUserConfig = {
1618
mnemonic,
1719
path: "m/44'/60'/0'/0",
1820
},
21+
gasPrice: 1000000000,
1922
},
23+
// for testnet
24+
//'sepolia': {
25+
// url: 'https://sepolia.base.org',
26+
// accounts: [process.env.WALLET_KEY as string],
27+
//gasPrice: 1000000000,
28+
//},
2029
},
2130
};
2231
export default config;

0 commit comments

Comments
 (0)