diff --git a/packages/contracts/contracts/NoteStream.sol b/packages/contracts/contracts/NoteStream.sol index 60bcd76..8f1a5fd 100644 --- a/packages/contracts/contracts/NoteStream.sol +++ b/packages/contracts/contracts/NoteStream.sol @@ -1,11 +1,11 @@ pragma solidity ^0.5.11; -import "@openzeppelin/contracts/lifecycle/Pausable.sol"; -import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import '@openzeppelin/contracts/lifecycle/Pausable.sol'; +import '@openzeppelin/contracts/utils/ReentrancyGuard.sol'; -import "./StreamUtilities.sol"; +import './StreamUtilities.sol'; -import "./Types.sol"; +import './Types.sol'; /** @@ -65,7 +65,7 @@ contract NoteStream is Pausable, ReentrancyGuard { require( msg.sender == streams[streamId].sender || msg.sender == streams[streamId].recipient, - "caller is not the sender or the recipient of the stream" + 'caller is not the sender or the recipient of the stream' ); _; } @@ -76,7 +76,7 @@ contract NoteStream is Pausable, ReentrancyGuard { modifier onlyRecipient(uint256 streamId) { require( msg.sender == streams[streamId].recipient, - "caller is not the recipient of the stream" + 'caller is not the recipient of the stream' ); _; } @@ -85,7 +85,7 @@ contract NoteStream is Pausable, ReentrancyGuard { * @dev Throws if the provided id does not point to a valid stream. */ modifier streamExists(uint256 streamId) { - require(streams[streamId].isEntity, "stream does not exist"); + require(streams[streamId].isEntity, 'stream does not exist'); _; } @@ -94,7 +94,7 @@ contract NoteStream is Pausable, ReentrancyGuard { constructor(address _aceContractAddress) public { require( _aceContractAddress != address(0x00), - "ACE contract is the zero address" + 'ACE contract is the zero address' ); aceContractAddress = _aceContractAddress; nextStreamId = 1; @@ -158,14 +158,14 @@ contract NoteStream is Pausable, ReentrancyGuard { uint256 startTime, uint256 stopTime ) public whenNotPaused returns (uint256) { - require(recipient != address(0x00), "stream to the zero address"); - require(recipient != address(this), "stream to the contract itself"); - require(recipient != msg.sender, "stream to the caller"); + require(recipient != address(0x00), 'stream to the zero address'); + require(recipient != address(this), 'stream to the contract itself'); + require(recipient != msg.sender, 'stream to the caller'); require( startTime >= block.timestamp, // solium-disable-line security/no-block-members - "start time before block.timestamp" + 'start time before block.timestamp' ); - require(stopTime > startTime, "Stream duration not greater than zero"); + require(stopTime > startTime, 'Stream duration not greater than zero'); // Transfer the ZkAsset to the streaming contract bytes32 streamNoteHash = StreamUtilities._processDeposit( @@ -211,27 +211,21 @@ contract NoteStream is Pausable, ReentrancyGuard { bytes memory _proof1, // Dividend Proof bytes memory _proof2, // Join-Split Proof uint256 _streamDurationToWithdraw - ) - public - nonReentrant - streamExists(streamId) - onlyRecipient(streamId) - { + ) public nonReentrant streamExists(streamId) onlyRecipient(streamId) { Types.AztecStream storage stream = streams[streamId]; // First check that this isn't a zero value withdrawal - require(_streamDurationToWithdraw > 0, "zero value withdrawal"); + require(_streamDurationToWithdraw > 0, 'zero value withdrawal'); // Check that fraction to withdraw isn't greater than fraction of time passed require( stream.lastWithdrawTime.add(_streamDurationToWithdraw) < block.timestamp, // solium-disable-line security/no-block-members - "withdraw is greater than allowed" + 'withdraw is greater than allowed' ); // Check that value of withdrawal matches the fraction given by the above timestamp - (, bytes memory _proof1OutputNotes) = StreamUtilities - ._validateRatioProof( + bytes32 withdrawalNoteHash = StreamUtilities._validateRatioProof( aceContractAddress, _proof1, _streamDurationToWithdraw, @@ -243,7 +237,7 @@ contract NoteStream is Pausable, ReentrancyGuard { bytes32 newNoteHash = StreamUtilities._processWithdrawal( aceContractAddress, _proof2, - _proof1OutputNotes, + withdrawalNoteHash, stream ); @@ -294,7 +288,7 @@ contract NoteStream is Pausable, ReentrancyGuard { } // We require the denominator of ratio proof to be nonzero - require(_unclaimedTime > 0, "cancellation with zero unclaimed time"); + require(_unclaimedTime > 0, 'cancellation with zero unclaimed time'); // Otherwise check that cancelling party isn't trying to scam the other // Each party can only cancel from a timestamp favourable to the other party. @@ -307,20 +301,19 @@ contract NoteStream is Pausable, ReentrancyGuard { stream.lastWithdrawTime.add(_unclaimedTime) > block.timestamp || stream.lastWithdrawTime.add(_unclaimedTime) == stream.stopTime, - "sender receives too much from cancellation" + 'sender receives too much from cancellation' ); } else if (msg.sender == stream.recipient) { // Recipient can only cancel from a timestamp which has already passed require( // solium-disable-next-line security/no-block-members stream.lastWithdrawTime.add(_unclaimedTime) < block.timestamp, - "recipient receives too much from cancellation" + 'recipient receives too much from cancellation' ); } // Check that value of withdrawal matches the fraction given by the above timestamp - (, bytes memory _proof1OutputNotes) = StreamUtilities - ._validateRatioProof( + bytes32 withdrawalNoteHash = StreamUtilities._validateRatioProof( aceContractAddress, _proof1, _unclaimedTime, @@ -332,7 +325,7 @@ contract NoteStream is Pausable, ReentrancyGuard { StreamUtilities._processCancelation( aceContractAddress, _proof2, - _proof1OutputNotes, + withdrawalNoteHash, stream ); diff --git a/packages/contracts/contracts/StreamUtilities.sol b/packages/contracts/contracts/StreamUtilities.sol index 51e2332..ede09df 100644 --- a/packages/contracts/contracts/StreamUtilities.sol +++ b/packages/contracts/contracts/StreamUtilities.sol @@ -1,13 +1,13 @@ pragma solidity ^0.5.11; -import "@openzeppelin/contracts/math/SafeMath.sol"; +import '@openzeppelin/contracts/math/SafeMath.sol'; -import "@aztec/protocol/contracts/interfaces/IACE.sol"; -import "@aztec/protocol/contracts/interfaces/IZkAsset.sol"; -import "@aztec/protocol/contracts/libs/NoteUtils.sol"; -import "@aztec/protocol/contracts/libs/MetaDataUtils.sol"; +import '@aztec/protocol/contracts/interfaces/IACE.sol'; +import '@aztec/protocol/contracts/interfaces/IZkAsset.sol'; +import '@aztec/protocol/contracts/libs/NoteUtils.sol'; +import '@aztec/protocol/contracts/libs/MetaDataUtils.sol'; -import "./Types.sol"; +import './Types.sol'; library StreamUtilities { @@ -60,18 +60,22 @@ library StreamUtilities { address _tokenAddress ) internal returns (bytes32 streamNoteHash) { // Validate Join-Split proof - bytes memory proofOutputs = IACE(_aceContractAddress) - .validateProof(JOIN_SPLIT_PROOF, address(this), _proof) - .get(0); + bytes memory proofOutputs = IACE(_aceContractAddress).validateProof( + JOIN_SPLIT_PROOF, + msg.sender, + _proof + ); + + bytes memory proofOutput = proofOutputs.get(0); // Extract notes used in proof - (, bytes memory _proofOutputNotes, , ) = proofOutputs + (, bytes memory _proofOutputNotes, , ) = proofOutput .extractProofOutput(); // Ensure that there is only a single output note to avoid loss of funds require( _proofOutputNotes.getLength() == 1, - "Incorrect number of output notes" + 'Incorrect number of output notes' ); Note memory streamNote = _noteCoderToStruct(_proofOutputNotes.get(0)); @@ -79,7 +83,7 @@ library StreamUtilities { // Require that stream note is owned by contract require( streamNote.owner == address(this), - "stream note is not owned by stream contract" + 'stream note is not owned by stream contract' ); // Require that sender and receiver have view access to stream note @@ -92,7 +96,7 @@ library StreamUtilities { "stream recipient can't view stream note" ); - // Approve contract to spend stream note + // Approve contract to place user's zkAssets into the stream note IZkAsset(_tokenAddress).approveProof( JOIN_SPLIT_PROOF, proofOutputs, @@ -104,7 +108,7 @@ library StreamUtilities { // Send transfer IZkAsset(_tokenAddress).confidentialTransferFrom( JOIN_SPLIT_PROOF, - proofOutputs + proofOutput ); // Return stream note hash @@ -116,19 +120,13 @@ library StreamUtilities { bytes memory _proof1, uint256 _withdrawDuration, Types.AztecStream storage _stream - ) - internal - returns ( - bytes memory _proof1InputNotes, - bytes memory _proof1OutputNotes - ) - { + ) internal returns (bytes32 withdrawalNoteHash) { // Check that ratio of notes match that given by fraction of remaining time to withdraw uint256 totalTime = _stream.stopTime.sub(_stream.lastWithdrawTime); require( getRatio(_proof1) == totalTime.mul(scalingFactor).div(_withdrawDuration), - "ratios do not match" + 'ratios do not match' ); // Validate ratio proof @@ -137,17 +135,23 @@ library StreamUtilities { address(this), _proof1 ); - (_proof1InputNotes, _proof1OutputNotes, , ) = _proof1Outputs - .get(0) - .extractProofOutput(); + ( + bytes memory _proof1InputNotes, + bytes memory _proof1OutputNotes, + , + + ) = _proof1Outputs.get(0).extractProofOutput(); // Make sure that recipient has provided the note on the contract as input // This prevents recipient using a larger note for this proof to allow a larger withdrawal require( _noteCoderToStruct(_proof1InputNotes.get(0)).noteHash == _stream.noteHash, - "incorrect notional note in proof 1" + 'incorrect notional note in proof 1' ); + + withdrawalNoteHash = _noteCoderToStruct(_proof1OutputNotes.get(0)) + .noteHash; } function _validateJoinSplitProof( @@ -169,45 +173,45 @@ library StreamUtilities { int256 publicValue ) = proof2Outputs.extractProofOutput(); - require(publicValue == 0, "nonzero public value transfer"); + require(publicValue == 0, 'nonzero public value transfer'); // Ensure stream note is the only input note require( _proof2InputNotes.getLength() == 1, - "Incorrect number of input notes" + 'Incorrect number of input notes' ); // Ensure that there isn't a third output note used to avoid the below checks require( _proof2OutputNotes.getLength() == 2, - "Incorrect number of output notes" + 'Incorrect number of output notes' ); // Requires that output note respects dividend proof require( _noteCoderToStruct(_proof2OutputNotes.get(0)).noteHash == _withdrawalNoteHash, - "withdraw note in 2 is not the same as 1" + 'withdraw note in 2 is not the same as 1' ); // Require that input note is stream note require( _noteCoderToStruct(_proof2InputNotes.get(0)).noteHash == _stream.noteHash, - "stream note in 2 is not correct" + 'stream note in 2 is not correct' ); } function _processWithdrawal( address _aceContractAddress, bytes memory _proof2, - bytes memory _proof1OutputNotes, + bytes32 _withdrawalNoteHash, Types.AztecStream storage _stream ) internal returns (bytes32) { bytes memory proof2Outputs = _validateJoinSplitProof( _aceContractAddress, _proof2, - _noteCoderToStruct(_proof1OutputNotes.get(0)).noteHash, // withdrawal note hash + _withdrawalNoteHash, _stream ); @@ -225,7 +229,7 @@ library StreamUtilities { // Require that change note is owned by contract require( newStreamNote.owner == address(this), - "change note in 2 is not owned by stream contract" + 'change note in 2 is not owned by stream contract' ); // Require that sender and receiver have view access to change note @@ -245,7 +249,7 @@ library StreamUtilities { _noteCoderToStruct(_proof2InputNotes.get(0)).noteHash, address(this), true, - "" + '' ); // Send transfer @@ -261,13 +265,13 @@ library StreamUtilities { function _processCancelation( address _aceContractAddress, bytes memory _proof2, - bytes memory _proof1OutputNotes, + bytes32 _withdrawalNoteHash, Types.AztecStream storage _stream ) internal returns (bool) { bytes memory proof2Outputs = _validateJoinSplitProof( _aceContractAddress, _proof2, - _noteCoderToStruct(_proof1OutputNotes.get(0)).noteHash, // withdrawal note hash + _withdrawalNoteHash, _stream ); // Extract notes used in proof @@ -281,24 +285,39 @@ library StreamUtilities { bytes32 inputNoteHash = _noteCoderToStruct(_proof2InputNotes.get(0)) .noteHash; + Note memory withdrawalNote = _noteCoderToStruct( + _proof2OutputNotes.get(0) + ); + Note memory refundNote = _noteCoderToStruct(_proof2OutputNotes.get(1)); + // Require that each participant owns an output note require( - _noteCoderToStruct(_proof2OutputNotes.get(0)).owner == - _stream.recipient, + withdrawalNote.owner == _stream.recipient, "Stream recipient doesn't own first output note" ); require( - _noteCoderToStruct(_proof2OutputNotes.get(1)).owner == - _stream.sender, + refundNote.owner == _stream.sender, "Stream sender doesn't own second output note" ); + // Require that sender and receiver have view access to their notes + require( + MetaDataUtils.extractAddress(withdrawalNote.metaData, 0) == + _stream.recipient, + "stream recipient can't view withdrawal note" + ); + require( + MetaDataUtils.extractAddress(refundNote.metaData, 0) == + _stream.sender, + "stream sender can't view refund note" + ); + // Approve contract to spend with stream note IZkAsset(_stream.tokenAddress).confidentialApprove( inputNoteHash, address(this), true, - "" + '' ); // Send transfer diff --git a/packages/contracts/contracts/mocks/StreamUtilitiesMock.sol b/packages/contracts/contracts/mocks/StreamUtilitiesMock.sol index 26ada8f..3dd59a1 100644 --- a/packages/contracts/contracts/mocks/StreamUtilitiesMock.sol +++ b/packages/contracts/contracts/mocks/StreamUtilitiesMock.sol @@ -1,11 +1,10 @@ pragma solidity ^0.5.11; pragma experimental ABIEncoderV2; -import "../StreamUtilities.sol"; +import '../StreamUtilities.sol'; contract StreamUtilitiesMock { - // The provided struct object is stored here as StreamUtilities expects a storage variable. Types.AztecStream public stream; @@ -14,7 +13,7 @@ contract StreamUtilitiesMock { pure returns (uint256 ratio) { - return StreamUtilities.getRatio(_proofData); + return StreamUtilities.getRatio(_proofData); } function validateRatioProof( @@ -22,12 +21,15 @@ contract StreamUtilitiesMock { bytes memory _proof1, uint256 _withdrawDuration, Types.AztecStream memory _stream - ) - public - returns (bytes memory, bytes memory) - { + ) public returns (bytes32) { stream = _stream; - return StreamUtilities._validateRatioProof(_aceContractAddress, _proof1, _withdrawDuration, stream); + return + StreamUtilities._validateRatioProof( + _aceContractAddress, + _proof1, + _withdrawDuration, + stream + ); } function validateJoinSplitProof( @@ -35,28 +37,65 @@ contract StreamUtilitiesMock { bytes memory _proof2, bytes32 _withdrawalNoteHash, Types.AztecStream memory _stream - ) public returns (bytes memory proof2Outputs) { + ) public returns (bytes memory) { stream = _stream; - return StreamUtilities._validateJoinSplitProof(_aceContractAddress, _proof2, _withdrawalNoteHash, stream); + return + StreamUtilities._validateJoinSplitProof( + _aceContractAddress, + _proof2, + _withdrawalNoteHash, + stream + ); + } + + function processDeposit( + bytes memory _proof, + bytes memory _proofSignature, + address _aceContractAddress, + address _sender, + address _recipient, + address _tokenAddress + ) public returns (bytes32) { + return + StreamUtilities._processDeposit( + _proof, + _proofSignature, + _aceContractAddress, + _sender, + _recipient, + _tokenAddress + ); } function processWithdrawal( address _aceContractAddress, bytes memory _proof2, - bytes memory _proof1OutputNotes, + bytes32 _withdrawalNoteHash, Types.AztecStream memory _stream ) public returns (bytes32) { stream = _stream; - return StreamUtilities._processWithdrawal(_aceContractAddress, _proof2, _proof1OutputNotes, stream); + return + StreamUtilities._processWithdrawal( + _aceContractAddress, + _proof2, + _withdrawalNoteHash, + stream + ); } function processCancelation( address _aceContractAddress, bytes memory _proof2, - bytes memory _proof1OutputNotes, + bytes32 _proof1OutputNotes, Types.AztecStream memory _stream ) public returns (bool) { stream = _stream; - return StreamUtilities._processCancelation(_aceContractAddress, _proof2, _proof1OutputNotes, stream); + return + StreamUtilities._processCancelation( + _aceContractAddress, + _proof2, + _proof1OutputNotes, + stream + ); } } diff --git a/packages/contracts/contracts/test/Imports.sol b/packages/contracts/contracts/test/Imports.sol deleted file mode 100644 index 34f5978..0000000 --- a/packages/contracts/contracts/test/Imports.sol +++ /dev/null @@ -1,22 +0,0 @@ -pragma solidity ^0.5.11; - -import "@aztec/protocol/contracts/ACE/noteRegistry/epochs/201912/base/FactoryBase201912.sol"; -import "@aztec/protocol/contracts/ACE/ACE.sol"; -import "@aztec/protocol/contracts/ACE/validators/joinSplit/JoinSplit.sol"; -import "@aztec/protocol/contracts/ACE/validators/dividend/Dividend.sol"; - -// You might think this file is a bit odd, but let me explain. -// We only use some contracts in our tests, which means Truffle -// will not compile it for us, because it is from an external -// dependency. -// -// We are now left with three options: -// - Copy/paste these contracts -// - Run the tests with `truffle compile --all` on -// - Or trick Truffle by claiming we use it in a Solidity test -// -// You know which one I went for. - -contract Imports { - constructor() public {} -} diff --git a/packages/contracts/test/NoteStream/cancelStream.js b/packages/contracts/test/NoteStream/cancelStream.js index c49d70e..43c705e 100644 --- a/packages/contracts/test/NoteStream/cancelStream.js +++ b/packages/contracts/test/NoteStream/cancelStream.js @@ -9,6 +9,9 @@ const crypto = require('crypto'); const NoteStream = require('../../build/NoteStream.json'); const { noteStreamFixture } = require('../fixtures'); +const { mintZkAsset } = require('../helpers/mint/mintZkAssets'); +const { signProof } = require('../helpers/signProof'); +const { createStreamDepositProof } = require('../helpers/deposit/streamNote'); const { contextForStreamDidEnd, @@ -63,7 +66,6 @@ function testValidCancellation() { // ).to.be.revertedWith('stream does not exist'); // } ); - it('returns true'); } function runTests() { @@ -132,29 +134,56 @@ function runTests() { } describe('NoteStream - cancelStream', function () { + let ace; + let token; let noteStream; let zkAsset; beforeEach(async function () { - ({ noteStream, zkAsset } = await loadFixture(noteStreamFixture)); + ({ ace, token, noteStream, zkAsset } = await loadFixture( + noteStreamFixture + )); }); - const now = bigNumberify(moment().format('X')); - describe('when the stream exists', function () { let streamId; + const streamDeposit = 100000; beforeEach(async function () { - const startTime = now.add( - STANDARD_TIME_OFFSET.multipliedBy(2).toString() + const depositOutputNote = await mintZkAsset( + sender.address, + streamDeposit, + token, + zkAsset, + ace ); + + const { depositProof } = await createStreamDepositProof( + [depositOutputNote], + noteStream.address, + sender.address, + recipient.address, + 0 + ); + + const { data, signature } = signProof( + zkAsset, + depositProof, + noteStream.address, + sender.signingKey.privateKey + ); + + const now = bigNumberify(moment().format('X')); + const startTime = now.add(STANDARD_TIME_OFFSET.toString()); const stopTime = startTime.add(STANDARD_TIME_DELTA.toString()); - const notehash = crypto.randomBytes(32); + const tx = await noteStream.createStream( recipient.address, - notehash, + data, + signature, zkAsset.address, startTime, stopTime ); + const receipt = await tx.wait(); streamId = NoteStreamInterface.parseLog( receipt.logs[receipt.logs.length - 1] diff --git a/packages/contracts/test/NoteStream/createStream.js b/packages/contracts/test/NoteStream/createStream.js index ea8f02b..5bb0fef 100644 --- a/packages/contracts/test/NoteStream/createStream.js +++ b/packages/contracts/test/NoteStream/createStream.js @@ -3,10 +3,8 @@ const { waffle } = require('@nomiclabs/buidler'); const { use, expect } = require('chai'); const { solidity, createFixtureLoader } = require('ethereum-waffle'); const { bigNumberify, Interface } = require('ethers/utils'); - const { devConstants } = require('@notestream/dev-utils'); const moment = require('moment'); -const crypto = require('crypto'); const { // STANDARD_SALARY, @@ -17,7 +15,9 @@ const { const NoteStream = require('../../build/NoteStream.json'); const { noteStreamFixture } = require('../fixtures'); -const { createStreamDepositProof } = require('../helpers/streamNote'); +const { mintZkAsset } = require('../helpers/mint/mintZkAssets'); +const { signProof } = require('../helpers/signProof'); +const { createStreamDepositProof } = require('../helpers/deposit/streamNote'); use(solidity); @@ -29,36 +29,48 @@ describe('NoteStream - createStream', function () { const NoteStreamInterface = new Interface(NoteStream.abi); + let ace; + let token; let noteStream; let zkAsset; - let depositOutputNotes; + let streamNote; let data; - let signatures; + let signature; + const streamDeposit = 100000; beforeEach(async function () { - ({ noteStream, zkAsset, depositOutputNotes } = await loadFixture( + ({ ace, token, noteStream, zkAsset } = await loadFixture( noteStreamFixture )); - const { - depositProof, - depositInputOwnerAccounts, - } = await createStreamDepositProof( - [depositOutputNotes[0]], + + const depositOutputNote = await mintZkAsset( + sender.address, + streamDeposit, + token, + zkAsset, + ace + ); + + const { depositProof } = await createStreamDepositProof( + [depositOutputNote], noteStream.address, - sender.signingKey.privateKey, - recipient.signingKey.privateKey, + sender.address, + recipient.address, 0 ); - data = depositProof.encodeABI(zkAsset.address); - signatures = depositProof.constructSignatures( - zkAsset.address, - depositInputOwnerAccounts - ); + + [streamNote] = depositProof.outputNotes; + + ({ data, signature } = signProof( + zkAsset, + depositProof, + noteStream.address, + sender.signingKey.privateKey + )); }); const now = bigNumberify(moment().format('X')); const startTime = now.add(STANDARD_TIME_OFFSET.toString()); const stopTime = startTime.add(STANDARD_TIME_DELTA.toString()); - const notehash = crypto.randomBytes(32); describe('when not paused', function () { describe('when the recipient is valid', function () { @@ -66,7 +78,7 @@ describe('NoteStream - createStream', function () { const tx = await noteStream.createStream( recipient.address, data, - signatures, + signature, zkAsset.address, startTime, stopTime @@ -79,9 +91,7 @@ describe('NoteStream - createStream', function () { const streamObject = await noteStream.getStream(streamId); expect(streamObject.sender).to.equal(sender.address); expect(streamObject.recipient).to.equal(recipient.address); - expect(streamObject.noteHash).to.equal( - `0x${notehash.toString('hex')}` - ); + expect(streamObject.noteHash).to.equal(streamNote.noteHash); expect(streamObject.tokenAddress).to.equal(zkAsset.address); expect(streamObject.startTime).to.equal(startTime); @@ -94,7 +104,7 @@ describe('NoteStream - createStream', function () { await noteStream.createStream( recipient.address, data, - signatures, + signature, zkAsset.address, startTime, stopTime @@ -109,7 +119,7 @@ describe('NoteStream - createStream', function () { noteStream.createStream( recipient.address, data, - signatures, + signature, zkAsset.address, startTime, stopTime @@ -125,7 +135,7 @@ describe('NoteStream - createStream', function () { noteStream.createStream( recipient.address, data, - signatures, + signature, zkAsset.address, invalidStartTime, stopTime @@ -138,7 +148,7 @@ describe('NoteStream - createStream', function () { noteStream.createStream( recipient.address, data, - signatures, + signature, zkAsset.address, startTime, startTime @@ -154,7 +164,7 @@ describe('NoteStream - createStream', function () { noteStream.createStream( recipient.address, data, - signatures, + signature, zkAsset.address, startTime, invalidStopTime @@ -168,7 +178,7 @@ describe('NoteStream - createStream', function () { noteStream.createStream( sender.address, data, - signatures, + signature, zkAsset.address, startTime, stopTime @@ -181,7 +191,7 @@ describe('NoteStream - createStream', function () { noteStream.createStream( noteStream.address, data, - signatures, + signature, zkAsset.address, startTime, stopTime @@ -194,7 +204,7 @@ describe('NoteStream - createStream', function () { noteStream.createStream( ZERO_ADDRESS, data, - signatures, + signature, zkAsset.address, startTime, stopTime @@ -211,7 +221,7 @@ describe('NoteStream - createStream', function () { noteStream.createStream( recipient.address, data, - signatures, + signature, zkAsset.address, startTime, stopTime diff --git a/packages/contracts/test/NoteStream/withdrawFromStream.js b/packages/contracts/test/NoteStream/withdrawFromStream.js index 797d7dd..a34c1c1 100644 --- a/packages/contracts/test/NoteStream/withdrawFromStream.js +++ b/packages/contracts/test/NoteStream/withdrawFromStream.js @@ -1,10 +1,6 @@ const { waffle } = require('@nomiclabs/buidler'); const { use, expect } = require('chai'); -const { - solidity, - MockProvider, - createFixtureLoader, -} = require('ethereum-waffle'); +const { solidity, createFixtureLoader } = require('ethereum-waffle'); const { bigNumberify, Interface } = require('ethers/utils'); const { devConstants, mochaContexts } = require('@notestream/dev-utils'); @@ -13,6 +9,9 @@ const crypto = require('crypto'); const NoteStream = require('../../build/NoteStream.json'); const { noteStreamFixture } = require('../fixtures'); +const { mintZkAsset } = require('../helpers/mint/mintZkAssets'); +const { signProof } = require('../helpers/signProof'); +const { createStreamDepositProof } = require('../helpers/deposit/streamNote'); const { contextForStreamDidEnd, @@ -29,144 +28,63 @@ const { use(solidity); -function runTests() { - const { provider } = waffle; - const [sender, recipient] = provider.getWallets(); - const loadFixture = createFixtureLoader(provider, [sender, recipient]); - - const NoteStreamInterface = new Interface(NoteStream.abi); - - let noteStream; - let zkAsset; - let streamId; - beforeEach(async function () { - ({ noteStream, zkAsset } = await loadFixture(noteStreamFixture)); - const now = bigNumberify(moment().format('X')); - const startTime = now.add(STANDARD_TIME_OFFSET.toString()); - const stopTime = startTime.add(STANDARD_TIME_DELTA.toString()); - - const notehash = crypto.randomBytes(32); - const tx = await noteStream.createStream( - recipient.address, - notehash, - zkAsset.address, - startTime, - stopTime - ); - const receipt = await tx.wait(); - streamId = NoteStreamInterface.parseLog( - receipt.logs[receipt.logs.length - 1] - ).values.streamId; - noteStream = noteStream.connect(recipient); - }); - - describe('when the withdrawal amount is higher than 0', function () { - it('reverts if the stream did not start', async function () { - const dividendProof = crypto.randomBytes(1); - const joinSplitProof = crypto.randomBytes(1); - const withrawDuration = FIVE_UNITS.toString(10); - await expect( - noteStream.withdrawFromStream( - streamId, - dividendProof, - joinSplitProof, - withrawDuration - ) - ).to.be.revertedWith('withdraw is greater than allowed'); - }); - - contextForStreamDidStartButNotEnd(provider, function () { - it('reverts if the withdrawal amount exceeds the available balance', async function () { - const withdrawDuration = STANDARD_TIME_OFFSET.multipliedBy( - 2 - ).toString(10); - const dividendProof = crypto.randomBytes(1); - const joinSplitProof = crypto.randomBytes(1); - await expect( - noteStream.withdrawFromStream( - streamId, - dividendProof, - joinSplitProof, - withdrawDuration - ) - ).to.be.revertedWith('withdraw is greater than allowed'); - }); - it('updates the streams lastWithdrawTime parameter'); - it('emits a WithdrawFromStream event'); - }); - - contextForStreamDidEnd(provider, function () { - it('reverts if the withdrawal amount exceeds the available balance', async function () { - const withdrawDuration = bigNumberify( - STANDARD_TIME_DELTA.toString() - ) - .mul(2) - .toString(); - const dividendProof = crypto.randomBytes(1); - const joinSplitProof = crypto.randomBytes(1); - await expect( - noteStream.withdrawFromStream( - streamId, - dividendProof, - joinSplitProof, - withdrawDuration - ) - ).to.be.revertedWith('withdraw is greater than allowed'); - }); - describe('when the balance is not withdrawn in full', function () { - it('updates the streams lastWithdrawTime parameter'); - it('emits a WithdrawFromStream event'); - }); - describe('when the balance is withdrawn in full', function () { - it('updates the streams lastWithdrawTime parameter'); - it('emits a WithdrawFromStream event'); - }); - }); - }); - - it('reverts when the withdrawal amount is zero', async function () { - const dividendProof = crypto.randomBytes(1); - const joinSplitProof = crypto.randomBytes(1); - const withdrawalDuration = '0'; - await expect( - noteStream.withdrawFromStream( - streamId, - dividendProof, - joinSplitProof, - withdrawalDuration - ) - ).to.be.revertedWith('zero value withdrawal'); - }); -} - describe('NoteStream - withdrawFromStream', function () { - const provider = new MockProvider(); - const [sender, recipient] = provider.getWallets(); + const { provider } = waffle; + const [sender, recipient, attacker] = provider.getWallets(); const loadFixture = createFixtureLoader(provider, [sender, recipient]); const NoteStreamInterface = new Interface(NoteStream.abi); + let ace; + let token; let noteStream; let zkAsset; beforeEach(async function () { - ({ noteStream, zkAsset } = await loadFixture(noteStreamFixture)); + ({ ace, token, noteStream, zkAsset } = await loadFixture( + noteStreamFixture + )); }); describe('when the stream exists', function () { let streamId; + const streamDeposit = 100000; beforeEach(async function () { + const depositOutputNote = await mintZkAsset( + sender.address, + streamDeposit, + token, + zkAsset, + ace + ); + + const { depositProof } = await createStreamDepositProof( + [depositOutputNote], + noteStream.address, + sender.address, + recipient.address, + 0 + ); + + const { data, signature } = signProof( + zkAsset, + depositProof, + noteStream.address, + sender.signingKey.privateKey + ); + const now = bigNumberify(moment().format('X')); const startTime = now.add(STANDARD_TIME_OFFSET.toString()); const stopTime = startTime.add(STANDARD_TIME_DELTA.toString()); - const notehash = crypto.randomBytes(32); const tx = await noteStream.createStream( recipient.address, - notehash, + data, + signature, zkAsset.address, startTime, stopTime ); + const receipt = await tx.wait(); streamId = NoteStreamInterface.parseLog( receipt.logs[receipt.logs.length - 1] @@ -178,17 +96,105 @@ describe('NoteStream - withdrawFromStream', function () { const joinSplitProof = crypto.randomBytes(1); const withdrawDuration = 1; await expect( - noteStream.withdrawFromStream( - streamId, - dividendProof, - joinSplitProof, - withdrawDuration - ) + noteStream + .connect(sender) + .withdrawFromStream( + streamId, + dividendProof, + joinSplitProof, + withdrawDuration + ) ).to.be.revertedWith('caller is not the recipient of the stream'); }); describe('when the caller is the recipient of the stream', function () { - runTests(); + beforeEach(function () { + noteStream = noteStream.connect(recipient); + }); + + describe('when the withdrawal amount is higher than 0', function () { + it('reverts if the stream did not start', async function () { + const dividendProof = crypto.randomBytes(1); + const joinSplitProof = crypto.randomBytes(1); + const withrawDuration = FIVE_UNITS.toString(10); + await expect( + noteStream.withdrawFromStream( + streamId, + dividendProof, + joinSplitProof, + withrawDuration + ) + ).to.be.revertedWith('withdraw is greater than allowed'); + }); + + contextForStreamDidStartButNotEnd(provider, function () { + it('reverts if the withdrawal amount exceeds the available balance', async function () { + const withdrawDuration = STANDARD_TIME_OFFSET.multipliedBy( + 2 + ).toString(10); + const dividendProof = crypto.randomBytes(1); + const joinSplitProof = crypto.randomBytes(1); + await expect( + noteStream.withdrawFromStream( + streamId, + dividendProof, + joinSplitProof, + withdrawDuration + ) + ).to.be.revertedWith( + 'withdraw is greater than allowed' + ); + }); + it('updates the streams lastWithdrawTime parameter'); + it('emits a WithdrawFromStream event'); + }); + + contextForStreamDidEnd(provider, function () { + it('reverts if the withdrawal amount exceeds the available balance', async function () { + const withdrawDuration = bigNumberify( + STANDARD_TIME_DELTA.toString() + ) + .mul(2) + .toString(); + const dividendProof = crypto.randomBytes(1); + const joinSplitProof = crypto.randomBytes(1); + await expect( + noteStream.withdrawFromStream( + streamId, + dividendProof, + joinSplitProof, + withdrawDuration + ) + ).to.be.revertedWith( + 'withdraw is greater than allowed' + ); + }); + describe('when the balance is not withdrawn in full', function () { + it('updates the streams lastWithdrawTime parameter'); + it('emits a WithdrawFromStream event'); + }); + describe('when the balance is withdrawn in full', function () { + it('updates the streams lastWithdrawTime parameter'); + it('emits a WithdrawFromStream event'); + }); + }); + }); + + it('reverts when the withdrawal amount is zero', async function () { + const dividendProof = crypto.randomBytes(1); + const joinSplitProof = crypto.randomBytes(1); + const withdrawalDuration = '0'; + await expect( + noteStream + .connect(recipient) + .withdrawFromStream( + streamId, + dividendProof, + joinSplitProof, + withdrawalDuration + ) + ).to.be.revertedWith('zero value withdrawal'); + }); }); it('reverts when the caller is not the sender or the recipient of the stream', async function () { @@ -196,12 +202,14 @@ describe('NoteStream - withdrawFromStream', function () { const joinSplitProof = crypto.randomBytes(1); const withdrawDuration = 1; await expect( - noteStream.withdrawFromStream( - streamId, - dividendProof, - joinSplitProof, - withdrawDuration - ) + noteStream + .connect(attacker) + .withdrawFromStream( + streamId, + dividendProof, + joinSplitProof, + withdrawDuration + ) ).to.be.revertedWith('caller is not the recipient of the stream'); }); }); @@ -210,12 +218,12 @@ describe('NoteStream - withdrawFromStream', function () { const dividendProof = crypto.randomBytes(1); const joinSplitProof = crypto.randomBytes(1); const withdrawDuration = 1; - const streamId = bigNumberify(419863); + const badStreamId = bigNumberify(419863); await expect( noteStream .connect(recipient) .withdrawFromStream( - streamId, + badStreamId, dividendProof, joinSplitProof, withdrawDuration diff --git a/packages/contracts/test/StreamUtilities/getRatio.js b/packages/contracts/test/StreamUtilities/getRatio.js index b96fe77..1b763f3 100644 --- a/packages/contracts/test/StreamUtilities/getRatio.js +++ b/packages/contracts/test/StreamUtilities/getRatio.js @@ -1,8 +1,11 @@ const { waffle } = require('@nomiclabs/buidler'); -const { use } = require('chai'); +const { use, expect } = require('chai'); const { solidity, createFixtureLoader } = require('ethereum-waffle'); +const { DividendProof } = require('aztec.js'); const { StreamUtilitiesFixture } = require('../fixtures'); +const { createNote } = require('../helpers/notes/createNote'); + use(solidity); describe('StreamUtilities - getRatio', function () { @@ -16,5 +19,51 @@ describe('StreamUtilities - getRatio', function () { ({ streamUtilitiesMock } = await loadFixture(StreamUtilitiesFixture)); }); - it('returns the correct ratio'); + it('returns the correct ratio', async function () { + // 1 * 1000 = 3 * 333 + 1 + const inputNoteValue = 1000; + const withdrawNoteValue = 333; + const remainderNoteValue = 1; + const ratioNumerator = 1; + const ratioDenominator = 3; + + // This is defined on the contract + const scalingFactor = 1000000000; + + const inputNote = await createNote( + inputNoteValue, + streamUtilitiesMock.address, + [sender.address] + ); + + const withdrawNote = await createNote( + withdrawNoteValue, + sender.address, + [sender.address] + ); + + const remainderNote = await createNote( + remainderNoteValue, + streamUtilitiesMock.address, + [sender.address] + ); + + const badProofData = new DividendProof( + inputNote, + remainderNote, + withdrawNote, + streamUtilitiesMock.address, + ratioDenominator, + ratioNumerator + ).encodeABI(); + + const ratio = await streamUtilitiesMock.getRatio(badProofData); + + // We flip the ratio as we require an integer value + const expectedRatio = parseInt( + (ratioDenominator * scalingFactor) / ratioNumerator, + 10 + ); + expect(ratio).to.be.equal(expectedRatio); + }); }); diff --git a/packages/contracts/test/StreamUtilities/index.js b/packages/contracts/test/StreamUtilities/index.js index b904d14..d1e55e8 100644 --- a/packages/contracts/test/StreamUtilities/index.js +++ b/packages/contracts/test/StreamUtilities/index.js @@ -1,4 +1,5 @@ require('./getRatio.js'); +require('./processDeposit.js'); require('./validateRatioProof.js'); require('./validateJoinSplitProof.js'); require('./processWithdrawal.js'); diff --git a/packages/contracts/test/StreamUtilities/processCancellation.js b/packages/contracts/test/StreamUtilities/processCancellation.js index 7a10bf2..484d551 100644 --- a/packages/contracts/test/StreamUtilities/processCancellation.js +++ b/packages/contracts/test/StreamUtilities/processCancellation.js @@ -1,7 +1,11 @@ const { waffle } = require('@nomiclabs/buidler'); -const { use } = require('chai'); +const { use, expect } = require('chai'); const { solidity, createFixtureLoader } = require('ethereum-waffle'); +const { JoinSplitProof } = require('aztec.js'); + const { StreamUtilitiesFixture } = require('../fixtures'); +const { createNote } = require('../helpers/notes/createNote'); +const { mintZkAsset } = require('../helpers/mint/mintZkAssets'); use(solidity); @@ -10,16 +14,149 @@ describe('StreamUtilities - processCancellation', function () { const [sender, recipient] = provider.getWallets(); const loadFixture = createFixtureLoader(provider, [sender, recipient]); - // eslint-disable-next-line no-unused-vars + let ace; + let token; + let zkAsset; + let streamNote; let streamUtilitiesMock; + let streamObject; + const streamDeposit = 100000; beforeEach(async function () { - ({ streamUtilitiesMock } = await loadFixture(StreamUtilitiesFixture)); + ({ ace, streamUtilitiesMock, token, zkAsset } = await loadFixture( + StreamUtilitiesFixture + )); + + streamNote = await mintZkAsset( + streamUtilitiesMock.address, + streamDeposit, + token, + zkAsset, + ace + ); + + streamObject = { + noteHash: streamNote.noteHash, + startTime: 0, + lastWithdrawTime: 50, + stopTime: 100, + recipient: recipient.address, + sender: sender.address, + tokenAddress: zkAsset.address, + isEntity: true, + }; }); - it('reverts if withdraw note is not owned by recipient'); - it('reverts if refund note is not owned by sender'); - it('reverts if recipient does not have view access to the withdraw note'); - it('reverts if sender does not have view access to the refund note'); - it('emits a confidentialTransfer event'); - it('returns true'); + const runTest = async ( + withdrawNoteOwner, + withdrawNoteViewAccess, + refundNoteOwner, + refundNoteViewAccess, + error + ) => { + const withdrawalNote = await createNote( + streamNote.k.toNumber() / 4, + withdrawNoteOwner, + [withdrawNoteViewAccess] + ); + + const refundNote = await createNote( + (streamNote.k.toNumber() * 3) / 4, + refundNoteOwner, + [refundNoteViewAccess] + ); + + const badProof = new JoinSplitProof( + [streamNote], + [withdrawalNote, refundNote], + streamUtilitiesMock.address, + 0, + withdrawNoteOwner + ); + + const badProofData = badProof.encodeABI(zkAsset.address); + + await expect( + streamUtilitiesMock.processCancelation( + ace.address, + badProofData, + withdrawalNote.noteHash, + streamObject + ) + ).to.be.revertedWith(error); + }; + + it('reverts if withdraw note is not owned by recipient', async function () { + await runTest( + sender.address, // incorrect + recipient.address, + sender.address, + sender.address, + "Stream recipient doesn't own first output note" + ); + }); + + it('reverts if refund note is not owned by sender', async function () { + await runTest( + recipient.address, + recipient.address, + recipient.address, // incorrect + sender.address, + "Stream sender doesn't own second output note" + ); + }); + + it('reverts if recipient does not have view access to the withdraw note', async function () { + await runTest( + recipient.address, + sender.address, // incorrect + sender.address, + sender.address, + "stream recipient can't view withdrawal note" + ); + }); + + it('reverts if sender does not have view access to the refund note', async function () { + await runTest( + recipient.address, + recipient.address, + sender.address, + recipient.address, // incorrect + "stream sender can't view refund note" + ); + }); + + it('transfers the zkAssets to the sender and recipient', async function () { + const withdrawalNote = await createNote( + streamNote.k.toNumber() / 4, + recipient.address, + [recipient.address] + ); + + const refundNote = await createNote( + (streamNote.k.toNumber() * 3) / 4, + sender.address, + [sender.address] + ); + + const proof = new JoinSplitProof( + [streamNote], + [withdrawalNote, refundNote], + streamUtilitiesMock.address, + 0, + sender.address + ); + + const proofData = proof.encodeABI(zkAsset.address); + + await expect( + streamUtilitiesMock.processCancelation( + ace.address, + proofData, + withdrawalNote.noteHash, + streamObject + ) + ) + .to.emit(zkAsset, 'DestroyNote') + .withArgs(streamUtilitiesMock.address, streamNote.noteHash); + }); }); diff --git a/packages/contracts/test/StreamUtilities/processDeposit.js b/packages/contracts/test/StreamUtilities/processDeposit.js new file mode 100644 index 0000000..8bc8c4c --- /dev/null +++ b/packages/contracts/test/StreamUtilities/processDeposit.js @@ -0,0 +1,242 @@ +const { waffle } = require('@nomiclabs/buidler'); +const { use, expect } = require('chai'); +const { solidity, createFixtureLoader } = require('ethereum-waffle'); +const { JoinSplitProof } = require('aztec.js'); + +const { StreamUtilitiesFixture } = require('../fixtures'); +const { mintZkAsset } = require('../helpers/mint/mintZkAssets'); +const { createNote } = require('../helpers/notes/createNote'); +const { + getStreamNote, + createStreamDepositProof, +} = require('../helpers/deposit/streamNote'); +const { signProof } = require('../helpers/signProof'); + +use(solidity); + +describe('StreamUtilities - processDeposit', function () { + const { provider } = waffle; + const [sender, recipient] = provider.getWallets(); + const loadFixture = createFixtureLoader(provider, [sender, recipient]); + + let ace; + let token; + let zkAsset; + let depositOutputNote; + let streamUtilitiesMock; + const streamDeposit = 100000; + beforeEach(async function () { + ({ + ace, + streamUtilitiesMock, + token, + zkAsset, + // depositOutputNotes, + } = await loadFixture(StreamUtilitiesFixture)); + + depositOutputNote = await mintZkAsset( + sender.address, + streamDeposit, + token, + zkAsset, + ace + ); + }); + + it('reverts if there is no output note', async function () { + const badProof = new JoinSplitProof( + [depositOutputNote], + [], + sender.address, + streamDeposit, + sender.address + ); + const { data, signature } = signProof( + zkAsset, + badProof, + streamUtilitiesMock.address, + sender.signingKey.privateKey + ); + + await expect( + streamUtilitiesMock.processDeposit( + data, + signature, + ace.address, + sender.address, + recipient.address, + zkAsset.address + ) + ).to.be.revertedWith('Incorrect number of output notes'); + }); + it('reverts if there is more than one output note', async function () { + const note1 = await getStreamNote( + streamDeposit / 2, + streamUtilitiesMock.address, + sender.address, + recipient.address + ); + const note2 = await getStreamNote( + streamDeposit / 2, + streamUtilitiesMock.address, + sender.address, + recipient.address + ); + + const badProof = new JoinSplitProof( + [depositOutputNote], + [note1, note2], + sender.address, + 0, + sender.address + ); + + const { data, signature } = signProof( + zkAsset, + badProof, + streamUtilitiesMock.address, + sender.signingKey.privateKey + ); + + await expect( + streamUtilitiesMock.processDeposit( + data, + signature, + ace.address, + sender.address, + recipient.address, + zkAsset.address + ) + ).to.be.revertedWith('Incorrect number of output notes'); + }); + it('reverts if streamNote is not owned by the contract', async function () { + const badNote = await getStreamNote( + streamDeposit, + sender.address, + sender.address, + recipient.address + ); + + const badProof = new JoinSplitProof( + [depositOutputNote], + [badNote], + sender.address, + 0, + sender.address + ); + + const { data, signature } = signProof( + zkAsset, + badProof, + streamUtilitiesMock.address, + sender.signingKey.privateKey + ); + + await expect( + streamUtilitiesMock.processDeposit( + data, + signature, + ace.address, + sender.address, + recipient.address, + zkAsset.address + ) + ).to.be.revertedWith('stream note is not owned by stream contract'); + }); + it('reverts if sender does not have view access to the streamNote', async function () { + const badNote = await createNote( + streamDeposit, + streamUtilitiesMock.address, + [recipient.address] + ); + + const badProof = new JoinSplitProof( + [depositOutputNote], + [badNote], + sender.address, + 0, + sender.address + ); + + const { data, signature } = signProof( + zkAsset, + badProof, + streamUtilitiesMock.address, + sender.signingKey.privateKey + ); + + await expect( + streamUtilitiesMock.processDeposit( + data, + signature, + ace.address, + sender.address, + recipient.address, + zkAsset.address + ) + ).to.be.revertedWith("stream sender can't view stream note"); + }); + it('reverts if recipient does not have view access to the streamNote', async function () { + const badNote = await createNote( + streamDeposit, + streamUtilitiesMock.address, + [sender.address] + ); + + const badProof = new JoinSplitProof( + [depositOutputNote], + [badNote], + sender.address, + 0, + sender.address + ); + + const { data, signature } = signProof( + zkAsset, + badProof, + streamUtilitiesMock.address, + sender.signingKey.privateKey + ); + + await expect( + streamUtilitiesMock.processDeposit( + data, + signature, + ace.address, + sender.address, + recipient.address, + zkAsset.address + ) + ).to.be.revertedWith("stream recipient can't view stream note"); + }); + it('transfers the zkAssets to the contract', async function () { + const { depositProof } = await createStreamDepositProof( + [depositOutputNote], + streamUtilitiesMock.address, + sender.address, + recipient.address, + 0 + ); + + const { data, signature } = signProof( + zkAsset, + depositProof, + streamUtilitiesMock.address, + sender.signingKey.privateKey + ); + await expect( + streamUtilitiesMock.processDeposit( + data, + signature, + ace.address, + sender.address, + recipient.address, + zkAsset.address + ) + ) + .to.emit(zkAsset, 'DestroyNote') + .withArgs(sender.address, depositOutputNote.noteHash); + }); + + it('returns the hash of the new stream note'); +}); diff --git a/packages/contracts/test/StreamUtilities/processWithdrawal.js b/packages/contracts/test/StreamUtilities/processWithdrawal.js index 7d4cdbc..cd9dfac 100644 --- a/packages/contracts/test/StreamUtilities/processWithdrawal.js +++ b/packages/contracts/test/StreamUtilities/processWithdrawal.js @@ -1,7 +1,11 @@ const { waffle } = require('@nomiclabs/buidler'); -const { use } = require('chai'); +const { use, expect } = require('chai'); const { solidity, createFixtureLoader } = require('ethereum-waffle'); +const { JoinSplitProof } = require('aztec.js'); + const { StreamUtilitiesFixture } = require('../fixtures'); +const { mintZkAsset } = require('../helpers/mint/mintZkAssets'); +const { createNote } = require('../helpers/notes/createNote'); use(solidity); @@ -10,16 +14,213 @@ describe('StreamUtilities - processWithdrawal', function () { const [sender, recipient] = provider.getWallets(); const loadFixture = createFixtureLoader(provider, [sender, recipient]); - // eslint-disable-next-line no-unused-vars + let ace; + let token; + let zkAsset; + let streamNote; let streamUtilitiesMock; + let streamObject; + const streamDeposit = 100000; beforeEach(async function () { - ({ streamUtilitiesMock } = await loadFixture(StreamUtilitiesFixture)); + ({ ace, streamUtilitiesMock, token, zkAsset } = await loadFixture( + StreamUtilitiesFixture + )); + + streamNote = await mintZkAsset( + streamUtilitiesMock.address, + streamDeposit, + token, + zkAsset, + ace + ); + + streamObject = { + noteHash: streamNote.noteHash, + startTime: 0, + lastWithdrawTime: 50, + stopTime: 100, + recipient: recipient.address, + sender: sender.address, + tokenAddress: zkAsset.address, + isEntity: true, + }; + }); + + it('reverts if new streamNote is not owned by NoteStream contract', async function () { + const withdrawalNote = await createNote( + streamNote.k.toNumber() / 4, + recipient.address, + [recipient.address] + ); + + // Try to steal refund note by setting sender as owner + const refundNote = await createNote( + (streamNote.k.toNumber() * 3) / 4, + sender.address, + [sender.address, recipient.address] + ); + + const badProof = new JoinSplitProof( + [streamNote], + [withdrawalNote, refundNote], + streamUtilitiesMock.address, + 0, + sender.address + ); + const badProofData = badProof.encodeABI(zkAsset.address); + + await expect( + streamUtilitiesMock.processWithdrawal( + ace.address, + badProofData, + withdrawalNote.noteHash, + streamObject + ) + ).to.be.revertedWith( + 'change note in 2 is not owned by stream contract' + ); + }); + + // it('reverts if withdraw note is not owned by recipient', async function () { + // // Try to steal withdrawal note by setting sender as owner + // const badWithdrawalNote = await createNote( + // streamNote.k.toNumber() / 4, + // sender.address, + // [sender.address] + // ); + + // const refundNote = await createNote( + // (streamNote.k.toNumber() * 3) / 4, + // streamUtilitiesMock.address, + // [sender.address, recipient.address] + // ); + + // const badProof = new JoinSplitProof( + // [streamNote], + // [badWithdrawalNote, refundNote], + // streamUtilitiesMock.address, + // 0, + // sender.address + // ); + + // const badProofData = badProof.encodeABI(zkAsset.address); + + // await expect( + // streamUtilitiesMock.processWithdrawal( + // ace.address, + // badProofData, + // badWithdrawalNote.noteHash, + // streamObject + // ) + // ).to.be.revertedWith( + // 'change note in 2 is not owned by stream contract' + // ); + // }); + + it('reverts if sender does not have view access to the new streamNote', async function () { + // withdraw 1/4th of note + const withdrawalNote = await createNote( + streamNote.k.toNumber() / 4, + recipient.address, + [recipient.address] + ); + + // Here the sender has not given themself view access to the note + const badRefundNote = await createNote( + (streamNote.k.toNumber() * 3) / 4, + streamUtilitiesMock.address, + [recipient.address] + ); + + const badProof = new JoinSplitProof( + [streamNote], + [withdrawalNote, badRefundNote], + streamUtilitiesMock.address, + 0, + sender.address + ); + + const badProofData = badProof.encodeABI(zkAsset.address); + + await expect( + streamUtilitiesMock.processWithdrawal( + ace.address, + badProofData, + withdrawalNote.noteHash, + streamObject + ) + ).to.be.revertedWith("stream sender can't view new stream note"); + }); + + it('reverts if recipient does not have view access to the new streamNote', async function () { + // withdraw 1/4th of note + const withdrawalNote = await createNote( + streamNote.k.toNumber() / 4, + recipient.address, + [recipient.address] + ); + + // Here the sender has not given the recipient view access to the note + const badRefundNote = await createNote( + (streamNote.k.toNumber() * 3) / 4, + streamUtilitiesMock.address, + [sender.address] + ); + + const badProof = new JoinSplitProof( + [streamNote], + [withdrawalNote, badRefundNote], + streamUtilitiesMock.address, + 0, + sender.address + ); + + const badProofData = badProof.encodeABI(zkAsset.address); + + await expect( + streamUtilitiesMock.processWithdrawal( + ace.address, + badProofData, + withdrawalNote.noteHash, + streamObject + ) + ).to.be.revertedWith("stream recipient can't view new stream note"); + }); + + it('transfers the zkAssets to the sender and recipient', async function () { + const withdrawalNote = await createNote( + streamNote.k.toNumber() / 4, + recipient.address, + [recipient.address] + ); + + const refundNote = await createNote( + (streamNote.k.toNumber() * 3) / 4, + streamUtilitiesMock.address, + [sender.address, recipient.address] + ); + + const proof = new JoinSplitProof( + [streamNote], + [withdrawalNote, refundNote], + streamUtilitiesMock.address, + 0, + sender.address + ); + + const proofData = proof.encodeABI(zkAsset.address); + + await expect( + streamUtilitiesMock.processWithdrawal( + ace.address, + proofData, + withdrawalNote.noteHash, + streamObject + ) + ) + .to.emit(zkAsset, 'DestroyNote') + .withArgs(streamUtilitiesMock.address, streamNote.noteHash); }); - it('reverts if new streamNote is not owned by NoteStream contract'); - it('reverts if withdraw note is not owned by recipient'); - it('reverts if sender does not have view access to the new streamNote'); - it('reverts if recipient does not have view access to the new streamNote'); - it('emits a confidentialTransfer event'); - it('returns the hash of the first output note'); + it('returns the hash of the new stream note'); }); diff --git a/packages/contracts/test/StreamUtilities/validateJoinSplitProof.js b/packages/contracts/test/StreamUtilities/validateJoinSplitProof.js index 18c671e..d5336a6 100644 --- a/packages/contracts/test/StreamUtilities/validateJoinSplitProof.js +++ b/packages/contracts/test/StreamUtilities/validateJoinSplitProof.js @@ -1,7 +1,9 @@ const { waffle } = require('@nomiclabs/buidler'); -const { use } = require('chai'); +const { use, expect } = require('chai'); const { solidity, createFixtureLoader } = require('ethereum-waffle'); +const { JoinSplitProof } = require('aztec.js'); const { StreamUtilitiesFixture } = require('../fixtures'); +const { createNote } = require('../helpers/notes/createNote'); use(solidity); @@ -10,16 +12,240 @@ describe('StreamUtilities - validateJoinSplitProof', function () { const [sender, recipient] = provider.getWallets(); const loadFixture = createFixtureLoader(provider, [sender, recipient]); - // eslint-disable-next-line no-unused-vars + let ace; let streamUtilitiesMock; + let zkAsset; + let streamObjectTemplate; beforeEach(async function () { - ({ streamUtilitiesMock } = await loadFixture(StreamUtilitiesFixture)); + ({ ace, streamUtilitiesMock, zkAsset } = await loadFixture( + StreamUtilitiesFixture + )); + streamObjectTemplate = { + startTime: 0, + lastWithdrawTime: 50, + stopTime: 100, + recipient: recipient.address, + sender: sender.address, + tokenAddress: zkAsset.address, + isEntity: true, + }; + }); + + it('reverts if proof has a non-zero public value transfer', async function () { + const inputNoteValue = 1000; + const outputNoteValue = 250; + const publicValue = 500; + + const inputNote = await createNote( + inputNoteValue, + streamUtilitiesMock.address, + [sender.address] + ); + + const outputNote1 = await createNote(outputNoteValue, sender.address, [ + sender.address, + ]); + + const outputNote2 = await createNote( + outputNoteValue, + streamUtilitiesMock.address, + [sender.address] + ); + + const badProof = new JoinSplitProof( + [inputNote], + [outputNote1, outputNote2], + streamUtilitiesMock.address, + publicValue, + sender.address + ); + const badProofData = badProof.encodeABI(zkAsset.address); + + const streamObject = { + ...streamObjectTemplate, + noteHash: inputNote.noteHash, + }; + + await expect( + streamUtilitiesMock.validateJoinSplitProof( + ace.address, + badProofData, + outputNote1.noteHash, + streamObject + ) + ).to.be.revertedWith('nonzero public value transfer'); + }); + + it('reverts if proof does not have one input note only', async function () { + const inputNoteValue = 1000; + + const inputNote1 = await createNote( + inputNoteValue, + streamUtilitiesMock.address, + [sender.address] + ); + + const inputNote2 = await createNote( + inputNoteValue, + streamUtilitiesMock.address, + [sender.address] + ); + + // We only use one output note as we exect the function to revert before it's checked + const outputNote = await createNote( + 2 * inputNoteValue, + sender.address, + [sender.address] + ); + + const badProofData = new JoinSplitProof( + [inputNote1, inputNote2], + [outputNote], + streamUtilitiesMock.address, + 0, + sender.address + ).encodeABI(zkAsset.address); + + const streamObject = { + ...streamObjectTemplate, + noteHash: inputNote1.noteHash, + }; + + await expect( + streamUtilitiesMock.validateJoinSplitProof( + ace.address, + badProofData, + outputNote.noteHash, + streamObject + ) + ).to.be.revertedWith('Incorrect number of input notes'); + }); + + it('reverts if proof does not have two output notes only', async function () { + const noteValue = 1000; + + const inputNote = await createNote( + noteValue, + streamUtilitiesMock.address, + [sender.address] + ); + + const outputNote = await createNote(noteValue, sender.address, [ + sender.address, + ]); + + const badProofData = new JoinSplitProof( + [inputNote], + [outputNote], + streamUtilitiesMock.address, + 0, + sender.address + ).encodeABI(zkAsset.address); + + const streamObject = { + ...streamObjectTemplate, + noteHash: inputNote.noteHash, + }; + + await expect( + streamUtilitiesMock.validateJoinSplitProof( + ace.address, + badProofData, + outputNote.noteHash, + streamObject + ) + ).to.be.revertedWith('Incorrect number of output notes'); + }); + + it('reverts if proof does not use same withdraw note as dividend proof', async function () { + const inputNoteValue = 1000; + const outputNoteValue = 500; + + const inputNote = await createNote( + inputNoteValue, + streamUtilitiesMock.address, + [sender.address] + ); + + const outputNote1 = await createNote(outputNoteValue, sender.address, [ + sender.address, + ]); + + const outputNote2 = await createNote( + outputNoteValue, + streamUtilitiesMock.address, + [sender.address] + ); + + const badProofData = new JoinSplitProof( + [inputNote], + [outputNote1, outputNote2], + streamUtilitiesMock.address, + 0, + sender.address + ).encodeABI(zkAsset.address); + + const streamObject = { + ...streamObjectTemplate, + noteHash: inputNote.noteHash, + }; + + // Here we pass the hash of outputNote2 + // To be a valid proof we should have usedoutputNote1 + await expect( + streamUtilitiesMock.validateJoinSplitProof( + ace.address, + badProofData, + outputNote2.noteHash, + streamObject + ) + ).to.be.revertedWith('withdraw note in 2 is not the same as 1'); + }); + + it('reverts if proof does not use stream note as input', async function () { + const inputNoteValue = 1000; + const outputNoteValue = 500; + + const inputNote = await createNote( + inputNoteValue, + streamUtilitiesMock.address, + [sender.address] + ); + + const outputNote1 = await createNote(outputNoteValue, sender.address, [ + sender.address, + ]); + + const outputNote2 = await createNote( + outputNoteValue, + streamUtilitiesMock.address, + [sender.address] + ); + + const badProofData = new JoinSplitProof( + [inputNote], + [outputNote1, outputNote2], + streamUtilitiesMock.address, + 0, + sender.address + ).encodeABI(zkAsset.address); + + const streamObject = { + ...streamObjectTemplate, + // Here we pass the output note hash as the stream note hash + // This should invalidate the proof. + noteHash: outputNote1.noteHash, + }; + + await expect( + streamUtilitiesMock.validateJoinSplitProof( + ace.address, + badProofData, + outputNote1.noteHash, + streamObject + ) + ).to.be.revertedWith('stream note in 2 is not correct'); }); - it('reverts if proof has a non-zero public value transfer'); - it('reverts if proof does not have one input note only'); - it('reverts if proof does not have two output notes only'); - it('reverts if proof does not use same withdraw note as dividend proof'); - it('reverts if proof does not use stream note as input'); it('returns output notes of proof'); }); diff --git a/packages/contracts/test/StreamUtilities/validateRatioProof.js b/packages/contracts/test/StreamUtilities/validateRatioProof.js index 51b68e1..31df98c 100644 --- a/packages/contracts/test/StreamUtilities/validateRatioProof.js +++ b/packages/contracts/test/StreamUtilities/validateRatioProof.js @@ -1,6 +1,8 @@ const { waffle } = require('@nomiclabs/buidler'); -const { use } = require('chai'); +const { use, expect } = require('chai'); const { solidity, createFixtureLoader } = require('ethereum-waffle'); +const { DividendProof } = require('aztec.js'); +const { createNote } = require('../helpers/notes/createNote'); const { StreamUtilitiesFixture } = require('../fixtures'); use(solidity); @@ -10,13 +12,136 @@ describe('StreamUtilities - validateRatioProof', function () { const [sender, recipient] = provider.getWallets(); const loadFixture = createFixtureLoader(provider, [sender, recipient]); - // eslint-disable-next-line no-unused-vars + let ace; let streamUtilitiesMock; + let streamObjectTemplate; + let zkAsset; beforeEach(async function () { - ({ streamUtilitiesMock } = await loadFixture(StreamUtilitiesFixture)); + ({ ace, streamUtilitiesMock, zkAsset } = await loadFixture( + StreamUtilitiesFixture + )); + streamObjectTemplate = { + recipient: recipient.address, + sender: sender.address, + tokenAddress: zkAsset.address, + isEntity: true, + }; }); - it('reverts if proof ratio does not match withdrawal duration'); - it('reverts if proof does not use stream note as source'); - it('returns input and output notes of proof'); + it('reverts if proof ratio does not match withdrawal duration', async function () { + const inputNoteValue = 1000; + + const streamTotalDuration = 100; + const withdrawalDuration = 10; + + // We may then withdraw 1/10th of the note's value, i.e. 100 + // Instead we try to take half + const withdrawNoteValue = 500; + const remainderNoteValue = 0; + const ratioNumerator = 1; + const ratioDenominator = 2; + + const inputNote = await createNote( + inputNoteValue, + streamUtilitiesMock.address, + [sender.address] + ); + + const withdrawNote = await createNote( + withdrawNoteValue, + sender.address, + [sender.address] + ); + + const remainderNote = await createNote( + remainderNoteValue, + streamUtilitiesMock.address, + [sender.address] + ); + + const badProofData = new DividendProof( + inputNote, + remainderNote, + withdrawNote, + streamUtilitiesMock.address, + ratioDenominator, + ratioNumerator + ).encodeABI(); + + const streamObject = { + ...streamObjectTemplate, + noteHash: inputNote.noteHash, + startTime: 0, + lastWithdrawTime: 0, + stopTime: streamTotalDuration, + }; + + await expect( + streamUtilitiesMock.validateRatioProof( + ace.address, + badProofData, + withdrawalDuration, + streamObject + ) + ).to.be.revertedWith('ratios do not match'); + }); + + it('reverts if proof does not use stream note as source', async function () { + const inputNoteValue = 1000; + + const streamTotalDuration = 100; + const withdrawalDuration = 10; + + // We may then withdraw 1/10th of the note's value, i.e. 100 + const withdrawNoteValue = 100; + const remainderNoteValue = 0; + const ratioNumerator = 1; + const ratioDenominator = 10; + + const inputNote = await createNote( + inputNoteValue, + streamUtilitiesMock.address, + [sender.address] + ); + + const withdrawNote = await createNote( + withdrawNoteValue, + sender.address, + [sender.address] + ); + + const remainderNote = await createNote( + remainderNoteValue, + streamUtilitiesMock.address, + [sender.address] + ); + + const badProofData = new DividendProof( + inputNote, + remainderNote, + withdrawNote, + streamUtilitiesMock.address, + ratioDenominator, + ratioNumerator + ).encodeABI(); + + const streamObject = { + ...streamObjectTemplate, + noteHash: withdrawNote.noteHash, + startTime: 0, + lastWithdrawTime: 0, + stopTime: streamTotalDuration, + }; + + await expect( + streamUtilitiesMock.validateRatioProof( + ace.address, + badProofData, + withdrawalDuration, + streamObject + ) + ).to.be.revertedWith('incorrect notional note in proof 1'); + }); + + it('returns withdrawal note hash'); }); diff --git a/packages/contracts/test/fixtures.js b/packages/contracts/test/fixtures.js index 0ab6ae5..7d11278 100644 --- a/packages/contracts/test/fixtures.js +++ b/packages/contracts/test/fixtures.js @@ -1,7 +1,6 @@ const { ethers } = require('ethers'); const { deployContract } = require('ethereum-waffle'); -const { JoinSplitProof } = require('aztec.js'); const bn128 = require('@aztec/bn128'); const { proofs } = require('@aztec/dev-utils'); @@ -18,8 +17,6 @@ const ERC20Mintable = require('../build/ERC20Mintable.json'); const NoteStream = require('../build/NoteStream.json'); const StreamUtilitiesMock = require('../build/StreamUtilitiesMock.json'); -const { getDepositNotes } = require('./helpers/AZTEC'); - const generateFactoryId = (epoch, cryptoSystem, assetType) => { return epoch * 256 ** 2 + cryptoSystem * 256 ** 1 + assetType * 256 ** 0; }; @@ -73,41 +70,37 @@ async function zkAssetFixture(provider, [wallet]) { 1, ]); - const depositAmount = '10000000000000'; - - await token.mint(wallet.address, depositAmount); - await token.approve(ace.address, depositAmount); - - const { - depositInputNotes, - depositOutputNotes, - depositPublicValue, - depositInputOwnerAccounts, - } = await getDepositNotes([100000], wallet.signingKey.privateKey); - const publicValue = depositPublicValue * -1; - - const sender = wallet.address; - const publicOwner = wallet.address; - const proof = new JoinSplitProof( - depositInputNotes, - depositOutputNotes, - sender, - publicValue, - publicOwner - ); - const data = proof.encodeABI(zkAsset.address); - const signatures = proof.constructSignatures( - zkAsset.address, - depositInputOwnerAccounts - ); - - // Approve ACE to spend tokens held by the zkAsset contract - await ace.publicApprove(zkAsset.address, proof.hash, 100000); - - // convert some of sender's assets to zkAssets - await zkAsset['confidentialTransfer(bytes,bytes)'](data, signatures); - - // ethers.errors.setLogLevel('warn'); + // const depositAmount = '10000000000000'; + + // await token.mint(wallet.address, depositAmount); + // await token.approve(ace.address, depositAmount); + + // const { + // depositInputNotes, + // depositOutputNotes, + // depositPublicValue, + // depositInputOwnerAccounts, + // } = await getDepositNotes([100000], wallet.address); + // const publicValue = depositPublicValue * -1; + + // const proof = new JoinSplitProof( + // depositInputNotes, + // depositOutputNotes, + // wallet.address, + // publicValue, + // wallet.address + // ); + // const data = proof.encodeABI(zkAsset.address); + // const signatures = proof.constructSignatures( + // zkAsset.address, + // depositInputOwnerAccounts + // ); + + // // Approve ACE to spend tokens held by the zkAsset contract + // await ace.publicApprove(zkAsset.address, proof.hash, 100000); + + // // convert some of sender's assets to zkAssets + // await zkAsset['confidentialTransfer(bytes,bytes)'](data, signatures); return { ace, @@ -116,7 +109,7 @@ async function zkAssetFixture(provider, [wallet]) { baseFactory, token, zkAsset, - depositOutputNotes, + // depositOutputNotes, }; } @@ -159,6 +152,7 @@ async function StreamUtilitiesFixture(provider, [wallet]) { baseFactory, token, zkAsset, + depositOutputNotes, } = await zkAssetFixture(provider, [wallet]); const streamUtilitiesMock = await deployContract( @@ -173,6 +167,7 @@ async function StreamUtilitiesFixture(provider, [wallet]) { baseFactory, token, zkAsset, + depositOutputNotes, streamUtilitiesMock, }; } diff --git a/packages/contracts/test/helpers/AZTEC.js b/packages/contracts/test/helpers/AZTEC.js deleted file mode 100644 index 51e412a..0000000 --- a/packages/contracts/test/helpers/AZTEC.js +++ /dev/null @@ -1,112 +0,0 @@ -const { note } = require('aztec.js'); -const secp256k1 = require('@aztec/secp256k1'); - -/** - * Generate a set of notes, given the desired note values and account of the owner - * - * @method getNotesForAccount - * @param {Object} aztecAccount - Ethereum account that owns the notes to be created - * @param {Number[]} noteValues - array of note values, for which notes will be created - * @returns {Note[]} - array of notes - */ -const getNotesForAccount = async (aztecAccount, noteValues) => { - return Promise.all( - noteValues.map((noteValue) => - note.create(aztecAccount.publicKey, noteValue) - ) - ); -}; - -/** - * General purpose function that generates a set of notes to be used in a deposit join split proof. - * - * There are no inputNotes created in this function - it generates notes for a deposit proof i.e. a joinSplit - * where tokens are being converted into notes. - * - * Output notes are created. The values of these output notes is determined by the input argument - * depositOutputNoteValues - * - * @method getDepositNotes - * @param {Number[]} depositOutputNoteValues - array of note values, for which notes will be created - * @param {Number[]} depositOwnerPrivateKey - private key for address which will own the created notes - * @returns {Note[]} depositInputNotes - input notes for a deposit join split proof - * @returns {Note[]} depositOutputNotes - output notes for a deposit join split proof - * @returns {Object[]} depositInputOwnerAccounts - Ethereum accounts of the input note owners - * @returns {Object[]} depositOutputOwnerAccounts - Ethereum accounts of the output note owners - */ -const getDepositNotes = async ( - depositOutputNoteValues, - depositOwnerPrivateKey -) => { - const owner = secp256k1.accountFromPrivateKey(depositOwnerPrivateKey); - const depositInputNotes = []; - const depositOutputNotes = await getNotesForAccount( - owner, - depositOutputNoteValues - ); - const depositPublicValue = depositOutputNoteValues.reduce((a, b) => a + b); - const depositInputOwnerAccounts = []; - const depositOutputOwnerAccounts = [owner]; - return { - depositInputNotes, - depositOutputNotes, - depositPublicValue, - depositInputOwnerAccounts, - depositOutputOwnerAccounts, - }; -}; - -/** - * General purpose function that generates a set of notes to be used in a deposit joinSplit proof - * followed by a transfer joinSplit proof. - * - * The scenario is that a deposit proof is being performed, followed by a transfer proof. - * In the deposit proof, public tokens are being converted into notes. - * - * These notes are then the input to a transfer proof, where notes are transferred to a second user. - * During this proof, some note value is also converted back into public token form and withdrawn. - * - * The value of the notes created and involved in the proofs is controlled through the two input arguments: - * depositOutputNoteValues and transferOutputNoteValues - * - * @method getDepositAndTransferNotes - * @param {Number[]} transferInputNoteValues - output note values for the deposit proof - * @param {Number[]} transferOutputNoteValues - output note values for the transfer proof - * @returns {Object[]} depositInputOwnerAccounts - Ethereum accounts of the deposit input note owners - * @returns {Object[]} depositOutputOwnerAccounts - Ethereum accounts of the deposit output note owners - * @returns {Note[]} transferInputNotes - inputs for a transfer join split proof - * @returns {Note[]} transferOutputNotes - output notes for a transfer join split proof - * @returns {Object[]} transferInputOwnerAccounts - Ethereum accounts of the transfer input note owners - * @returns {Object[]} transferOutputOwnerAccounts - Ethereum accounts of the transfer output note owners - * @returns {Note[]} notes - array of all notes created - * @returns {ownerAccounts} ownerAccounts - Ethereum accounts of the created notes - */ -const getTransferNotes = async ( - transferInputNotes, - transferOutputNoteValues, - senderPrivateKey, - recipientPrivateKey -) => { - const sender = secp256k1.accountFromPrivateKey(senderPrivateKey); - const recipient = secp256k1.accountFromPrivateKey(recipientPrivateKey); - - const transferInputOwnerAccounts = new Array( - transferInputNotes.length - ).fill(sender); - - const transferOutputNotes = await getNotesForAccount( - recipient, - transferOutputNoteValues - ); - const transferOutputOwnerAccounts = new Array( - transferOutputNotes.length - ).fill(recipient); - return { - transferInputNotes, - transferOutputNotes, - transferInputOwnerAccounts, - transferOutputOwnerAccounts, - }; -}; - -module.exports = { getNotesForAccount, getDepositNotes, getTransferNotes }; diff --git a/packages/contracts/test/helpers/constants.js b/packages/contracts/test/helpers/constants.js new file mode 100644 index 0000000..d42ab67 --- /dev/null +++ b/packages/contracts/test/helpers/constants.js @@ -0,0 +1,6 @@ +// Adding an address to the metadata requires a public key however as we are not decrypting notes from the registry +// we do not need to use a proper public key. We then use a constant dummy value. +const dummyLinkedPublicKey = + '0xa61d17b0dd3095664d264628a6b947721314b6999aa6a73d3c7698f041f78a4d'; + +module.exports = { dummyLinkedPublicKey }; diff --git a/packages/contracts/test/helpers/streamNote.js b/packages/contracts/test/helpers/deposit/streamNote.js similarity index 56% rename from packages/contracts/test/helpers/streamNote.js rename to packages/contracts/test/helpers/deposit/streamNote.js index 6249c5c..2e77c12 100644 --- a/packages/contracts/test/helpers/streamNote.js +++ b/packages/contracts/test/helpers/deposit/streamNote.js @@ -1,5 +1,5 @@ -const { note, JoinSplitProof } = require('aztec.js'); -const secp256k1 = require('@aztec/secp256k1'); +const { JoinSplitProof } = require('aztec.js'); +const { createNote } = require('../notes/createNote'); /** * Generate a note such that would be owned by the NoteStream contract, given the desired note value and stream sender/recipients @@ -10,34 +10,18 @@ const secp256k1 = require('@aztec/secp256k1'); * @param {Object} recipient - Ethereum account that is receiving the stream * @returns {Note} - stream note */ -const getStreamNote = async (noteValue, noteOwner, sender, recipient) => { - return note.create( - secp256k1.generateAccount().publicKey, - noteValue, - [ - { - address: sender.address, - linkedPublicKey: sender.publicKey, - }, - { - address: recipient.address, - linkedPublicKey: recipient.publicKey, - }, - ], - noteOwner - ); +const getStreamNote = (noteValue, noteOwner, sender, recipient) => { + return createNote(noteValue, noteOwner, [sender, recipient]); }; const createStreamDepositProof = async ( inputNotes, noteOwner, - senderPrivateKey, - recipientPrivateKey, + sender, + recipient, publicValue ) => { - const sender = secp256k1.accountFromPrivateKey(senderPrivateKey); - const recipient = secp256k1.accountFromPrivateKey(recipientPrivateKey); - console.log(sender, recipient); + // console.log(sender, recipient); const depositInputOwnerAccounts = new Array(inputNotes.length).fill(sender); const noteValue = inputNotes @@ -53,13 +37,14 @@ const createStreamDepositProof = async ( const depositProof = new JoinSplitProof( inputNotes, [streamNote], - sender.address, + sender, publicValue, - sender.address + sender ); return { depositProof, depositInputOwnerAccounts, + streamNote, }; }; diff --git a/packages/contracts/test/helpers/mint/getDepositNotes.js b/packages/contracts/test/helpers/mint/getDepositNotes.js new file mode 100644 index 0000000..b71d501 --- /dev/null +++ b/packages/contracts/test/helpers/mint/getDepositNotes.js @@ -0,0 +1,41 @@ +const { getNotesForAccount } = require('../notes/getNotesForAccount'); + +/** + * General purpose function that generates a set of notes to be used in a deposit join split proof. + * + * There are no inputNotes created in this function - it generates notes for a deposit proof i.e. a joinSplit + * where tokens are being converted into notes. + * + * Output notes are created. The values of these output notes is determined by the input argument + * depositOutputNoteValues + * + * @method getDepositNotes + * @param {Number[]} depositOutputNoteValues - array of note values, for which notes will be created + * @param {Number[]} depositOwnerPrivateKey - private key for address which will own the created notes + * @returns {Note[]} depositInputNotes - input notes for a deposit join split proof + * @returns {Note[]} depositOutputNotes - output notes for a deposit join split proof + * @returns {Object[]} depositInputOwnerAccounts - Ethereum accounts of the input note owners + * @returns {Object[]} depositOutputOwnerAccounts - Ethereum accounts of the output note owners + */ +const getDepositNotes = async ( + depositOutputNoteValues, + depositOwnerAddress +) => { + const depositInputNotes = []; + const depositOutputNotes = await getNotesForAccount( + depositOwnerAddress, + depositOutputNoteValues + ); + const depositPublicValue = depositOutputNoteValues.reduce((a, b) => a + b); + const depositInputOwnerAccounts = []; + const depositOutputOwnerAccounts = [depositOwnerAddress]; + return { + depositInputNotes, + depositOutputNotes, + depositPublicValue, + depositInputOwnerAccounts, + depositOutputOwnerAccounts, + }; +}; + +module.exports = { getDepositNotes }; diff --git a/packages/contracts/test/helpers/mint/mintZkAssets.js b/packages/contracts/test/helpers/mint/mintZkAssets.js new file mode 100644 index 0000000..3007205 --- /dev/null +++ b/packages/contracts/test/helpers/mint/mintZkAssets.js @@ -0,0 +1,35 @@ +const { JoinSplitProof } = require('aztec.js'); +const { getDepositNotes } = require('./getDepositNotes'); + +const mintZkAsset = async (recipientAddress, amount, token, zkAsset, ace) => { + const signerAddress = token.signer.address; + // Mint public tokens and give ACE access to move them + await token.mint(signerAddress, amount); + await token.approve(ace.address, amount); + + const { + depositInputNotes, + depositOutputNotes, + depositPublicValue, + } = await getDepositNotes([amount], recipientAddress); + const publicValue = depositPublicValue * -1; + + const proof = new JoinSplitProof( + depositInputNotes, + depositOutputNotes, + signerAddress, + publicValue, + signerAddress + ); + const data = proof.encodeABI(zkAsset.address); + + // Approve ACE to spend tokens held by the zkAsset contract + await ace.publicApprove(zkAsset.address, proof.hash, amount); + + // Note: As there are no input notes we can use an empty signature + await zkAsset['confidentialTransfer(bytes,bytes)'](data, '0x'); + + return depositOutputNotes[0]; +}; + +module.exports = { mintZkAsset }; diff --git a/packages/contracts/test/helpers/notes/createNote.js b/packages/contracts/test/helpers/notes/createNote.js new file mode 100644 index 0000000..ebc45ba --- /dev/null +++ b/packages/contracts/test/helpers/notes/createNote.js @@ -0,0 +1,17 @@ +const { note } = require('aztec.js'); +const secp256k1 = require('@aztec/secp256k1'); +const { dummyLinkedPublicKey } = require('../constants'); + +const createNote = (noteValue, noteOwner, access) => { + return note.create( + secp256k1.generateAccount().publicKey, + noteValue, + access.map((address) => ({ + address, + linkedPublicKey: dummyLinkedPublicKey, + })), + noteOwner + ); +}; + +module.exports = { createNote }; diff --git a/packages/contracts/test/helpers/notes/getNotesForAccount.js b/packages/contracts/test/helpers/notes/getNotesForAccount.js new file mode 100644 index 0000000..b9a6d8c --- /dev/null +++ b/packages/contracts/test/helpers/notes/getNotesForAccount.js @@ -0,0 +1,17 @@ +const { createNote } = require('./createNote'); + +/** + * Generate a set of notes, given the desired note values and account of the owner + * + * @method getNotesForAccount + * @param {Object} aztecAccount - Ethereum account that owns the notes to be created + * @param {Number[]} noteValues - array of note values, for which notes will be created + * @returns {Note[]} - array of notes + */ +const getNotesForAccount = async (address, noteValues) => { + return Promise.all( + noteValues.map((noteValue) => createNote(noteValue, address, [address])) + ); +}; + +module.exports = { getNotesForAccount }; diff --git a/packages/contracts/test/helpers/signProof.js b/packages/contracts/test/helpers/signProof.js new file mode 100644 index 0000000..c77c9ae --- /dev/null +++ b/packages/contracts/test/helpers/signProof.js @@ -0,0 +1,15 @@ +const { signer } = require('aztec.js'); + +const signProof = (zkAsset, proof, spender, privateKey) => { + const data = proof.encodeABI(zkAsset.address); + const signature = signer.signApprovalForProof( + zkAsset.address, + proof.eth.outputs, + spender, + true, + privateKey + ); + return { data, signature }; +}; + +module.exports = { signProof }; diff --git a/packages/contracts/test/helpers/transfer/getTransferNotes.js b/packages/contracts/test/helpers/transfer/getTransferNotes.js new file mode 100644 index 0000000..0f34022 --- /dev/null +++ b/packages/contracts/test/helpers/transfer/getTransferNotes.js @@ -0,0 +1,51 @@ +const { getNotesForAccount } = require('../notes/getNotesForAccount'); + +/** + * General purpose function that generates a set of notes to be used in a deposit joinSplit proof + * followed by a transfer joinSplit proof. + * + * The scenario is that a deposit proof is being performed, followed by a transfer proof. + * In the deposit proof, public tokens are being converted into notes. + * + * These notes are then the input to a transfer proof, where notes are transferred to a second user. + * During this proof, some note value is also converted back into public token form and withdrawn. + * + * The value of the notes created and involved in the proofs is controlled through the two input arguments: + * depositOutputNoteValues and transferOutputNoteValues + * + * @method getDepositAndTransferNotes + * @param {Number[]} transferInputNoteValues - output note values for the deposit proof + * @param {Number[]} transferOutputNoteValues - output note values for the transfer proof + * @param {Number[]} transferInputNoteValues - output note values for the deposit proof + * @param {Number[]} transferOutputNoteValues - output note values for the transfer proof + * @returns {Note[]} transferInputNotes - inputs for a transfer join split proof + * @returns {Note[]} transferOutputNotes - output notes for a transfer join split proof + * @returns {Object[]} transferInputOwnerAccounts - Ethereum accounts of the transfer input note owners + * @returns {Object[]} transferOutputOwnerAccounts - Ethereum accounts of the transfer output note owners + */ +const getTransferNotes = async ( + transferInputNotes, + transferOutputNoteValues, + senderAddress, + recipientAddress +) => { + const transferInputOwnerAccounts = new Array( + transferInputNotes.length + ).fill(senderAddress); + + const transferOutputNotes = await getNotesForAccount( + recipientAddress, + transferOutputNoteValues + ); + const transferOutputOwnerAccounts = new Array( + transferOutputNotes.length + ).fill(recipientAddress); + return { + transferInputNotes, + transferOutputNotes, + transferInputOwnerAccounts, + transferOutputOwnerAccounts, + }; +}; + +module.exports = { getTransferNotes };