From 264b78e0ae9a3f8c4159f4dc36599be44accdc6d Mon Sep 17 00:00:00 2001 From: Joey Kraut Date: Sat, 8 Mar 2025 16:07:34 -0800 Subject: [PATCH] test: darkpool: SettleAtomicMatch: Add native asset tests --- src/Darkpool.sol | 2 + src/libraries/darkpool/ExternalTransfers.sol | 8 +- test/darkpool/DarkpoolTestBase.sol | 9 +- test/darkpool/SettleAtomicMatch.t.sol | 136 ++++++++++++++++++- test/test-contracts/WethMock.sol | 20 +++ 5 files changed, 169 insertions(+), 6 deletions(-) create mode 100644 test/test-contracts/WethMock.sol diff --git a/src/Darkpool.sol b/src/Darkpool.sol index 90af7f8..40cf609 100644 --- a/src/Darkpool.sol +++ b/src/Darkpool.sol @@ -81,6 +81,7 @@ contract Darkpool { constructor( uint256 protocolFeeRate_, address protocolFeeRecipient_, + IWETH9 weth_, IHasher hasher_, IVerifier verifier_, IPermit2 permit2_ @@ -90,6 +91,7 @@ contract Darkpool { hasher = hasher_; verifier = verifier_; permit2 = permit2_; + weth = weth_; merkleTree.initialize(); } diff --git a/src/libraries/darkpool/ExternalTransfers.sol b/src/libraries/darkpool/ExternalTransfers.sol index 157509a..f2aabe5 100644 --- a/src/libraries/darkpool/ExternalTransfers.sol +++ b/src/libraries/darkpool/ExternalTransfers.sol @@ -18,9 +18,9 @@ import { IWETH9 } from "renegade/libraries/interfaces/IWETH9.sol"; import { ISignatureTransfer } from "permit2/interfaces/ISignatureTransfer.sol"; import { IERC20 } from "forge-std/interfaces/IERC20.sol"; -// @title TransferExecutor -// @notice This library implements the logic for executing external transfers -// @notice External transfers are either deposits or withdrawals into/from the darkpool +/// @title TransferExecutor +/// @notice This library implements the logic for executing external transfers +/// @notice External transfers are either deposits or withdrawals into/from the darkpool library TransferExecutor { using TypesLib for DepositWitness; @@ -133,7 +133,7 @@ library TransferExecutor { function executeSimpleDeposit(SimpleTransfer memory transfer, IWETH9 wrapper) internal { // Handle native token deposits by wrapping the transaction value if (DarkpoolConstants.isNativeToken(transfer.mint)) { - require(msg.value == transfer.amount, "Invalid ETH deposit amount"); + require(msg.value == transfer.amount, "msg.value does not match deposit amount"); wrapper.deposit{ value: transfer.amount }(); return; } diff --git a/test/darkpool/DarkpoolTestBase.sol b/test/darkpool/DarkpoolTestBase.sol index 1529dc3..2882aaf 100644 --- a/test/darkpool/DarkpoolTestBase.sol +++ b/test/darkpool/DarkpoolTestBase.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.0; import { BN254 } from "solidity-bn254/BN254.sol"; import { ERC20Mock } from "oz-contracts/mocks/token/ERC20Mock.sol"; +import { WethMock } from "../test-contracts/WethMock.sol"; import { IPermit2 } from "permit2/interfaces/IPermit2.sol"; import { DeployPermit2 } from "permit2-test/utils/DeployPermit2.sol"; import { Test } from "forge-std/Test.sol"; @@ -30,6 +31,7 @@ contract DarkpoolTestBase is CalldataUtils { IPermit2 public permit2; ERC20Mock public quoteToken; ERC20Mock public baseToken; + WethMock public weth; address public protocolFeeAddr; @@ -38,6 +40,7 @@ contract DarkpoolTestBase is CalldataUtils { bytes constant INVALID_SIGNATURE_REVERT_STRING = "Invalid signature"; bytes constant INVALID_PROTOCOL_FEE_REVERT_STRING = "Invalid protocol fee rate"; bytes constant INVALID_ETH_VALUE_REVERT_STRING = "Invalid ETH value, should be zero unless selling native token"; + bytes constant INVALID_ETH_DEPOSIT_AMOUNT_REVERT_STRING = "msg.value does not match deposit amount"; function setUp() public { // Deploy a Permit2 instance for testing @@ -47,12 +50,16 @@ contract DarkpoolTestBase is CalldataUtils { // Deploy mock tokens for testing quoteToken = new ERC20Mock(); baseToken = new ERC20Mock(); + weth = new WethMock(); + + // Capitalize the weth contract + vm.deal(address(weth), 100 ether); // Deploy the darkpool implementation contracts hasher = IHasher(HuffDeployer.deploy("libraries/poseidon2/poseidonHasher")); IVerifier verifier = new TestVerifier(); protocolFeeAddr = vm.randomAddress(); - darkpool = new Darkpool(TEST_PROTOCOL_FEE, protocolFeeAddr, hasher, verifier, permit2); + darkpool = new Darkpool(TEST_PROTOCOL_FEE, protocolFeeAddr, weth, hasher, verifier, permit2); } // --------------------------- diff --git a/test/darkpool/SettleAtomicMatch.t.sol b/test/darkpool/SettleAtomicMatch.t.sol index cca3916..bb92f56 100644 --- a/test/darkpool/SettleAtomicMatch.t.sol +++ b/test/darkpool/SettleAtomicMatch.t.sol @@ -16,11 +16,11 @@ import { ExternalMatchResult, FeeTake } from "renegade/libraries/darkpool/Types.sol"; +import { DarkpoolConstants } from "renegade/libraries/darkpool/Constants.sol"; import { ValidMatchSettleAtomicStatement, ValidWalletUpdateStatement } from "renegade/libraries/darkpool/PublicInputs.sol"; import { PlonkProof } from "renegade/libraries/verifier/Types.sol"; -import { console2 } from "forge-std/console2.sol"; contract SettleAtomicMatchTest is DarkpoolTestBase { using TypesLib for FeeTake; @@ -285,6 +285,111 @@ contract SettleAtomicMatchTest is DarkpoolTestBase { assertEq(darkpoolFinalBaseBalance, darkpoolInitialBaseBalance + BASE_AMT); } + /// @notice Test settling an atomic match with a native token, buy side + function test_settleAtomicMatch_nativeToken_buySide() public { + Vm.Wallet memory externalParty = randomEthereumWallet(); + + // Setup tokens + quoteToken.mint(externalParty.addr, QUOTE_AMT); + weth.mint(address(darkpool), BASE_AMT); + + uint256 userInitialQuoteBalance = quoteToken.balanceOf(externalParty.addr); + uint256 userInitialNativeBalance = externalParty.addr.balance; + uint256 darkpoolInitialQuoteBalance = quoteToken.balanceOf(address(darkpool)); + uint256 darkpoolInitialWethBalance = weth.balanceOf(address(darkpool)); + + // Setup the match + ExternalMatchResult memory matchResult = ExternalMatchResult({ + quoteMint: address(quoteToken), + baseMint: DarkpoolConstants.NATIVE_TOKEN_ADDRESS, + quoteAmount: QUOTE_AMT, + baseAmount: BASE_AMT, + direction: ExternalMatchDirection.InternalPartySell + }); + + // Setup calldata + BN254.ScalarField merkleRoot = darkpool.getMerkleRoot(); + ( + PartyMatchPayload memory internalPartyPayload, + ValidMatchSettleAtomicStatement memory statement, + MatchAtomicProofs memory proofs, + MatchAtomicLinkingProofs memory linkingProofs + ) = settleAtomicMatchCalldataWithMatchResult(merkleRoot, matchResult); + + // Process the match + vm.startBroadcast(externalParty.addr); + quoteToken.approve(address(darkpool), QUOTE_AMT); + darkpool.processAtomicMatchSettle(internalPartyPayload, statement, proofs, linkingProofs); + vm.stopBroadcast(); + + // Check the token flows + FeeTake memory fees = statement.externalPartyFees; + uint256 totalFee = fees.total(); + uint256 expectedBaseAmt = BASE_AMT - totalFee; + + uint256 userFinalQuoteBalance = quoteToken.balanceOf(externalParty.addr); + uint256 userFinalNativeBalance = externalParty.addr.balance; + uint256 darkpoolFinalQuoteBalance = quoteToken.balanceOf(address(darkpool)); + uint256 darkpoolFinalWethBalance = weth.balanceOf(address(darkpool)); + + assertEq(userFinalQuoteBalance, userInitialQuoteBalance - QUOTE_AMT); + assertEq(userFinalNativeBalance, userInitialNativeBalance + expectedBaseAmt); + assertEq(darkpoolFinalQuoteBalance, darkpoolInitialQuoteBalance + QUOTE_AMT); + assertEq(darkpoolFinalWethBalance, darkpoolInitialWethBalance - BASE_AMT); + } + + /// @notice Test settling an atomic match with a native token, sell side + function test_settleAtomicMatch_nativeToken_sellSide() public { + Vm.Wallet memory externalParty = randomEthereumWallet(); + + // Setup tokens + vm.deal(externalParty.addr, BASE_AMT); + quoteToken.mint(address(darkpool), QUOTE_AMT); + + uint256 userInitialQuoteBalance = quoteToken.balanceOf(externalParty.addr); + uint256 userInitialNativeBalance = externalParty.addr.balance; + uint256 darkpoolInitialQuoteBalance = quoteToken.balanceOf(address(darkpool)); + uint256 darkpoolInitialWethBalance = weth.balanceOf(address(darkpool)); + + // Setup the match + ExternalMatchResult memory matchResult = ExternalMatchResult({ + quoteMint: address(quoteToken), + baseMint: DarkpoolConstants.NATIVE_TOKEN_ADDRESS, + quoteAmount: QUOTE_AMT, + baseAmount: BASE_AMT, + direction: ExternalMatchDirection.InternalPartyBuy + }); + + // Setup calldata + BN254.ScalarField merkleRoot = darkpool.getMerkleRoot(); + ( + PartyMatchPayload memory internalPartyPayload, + ValidMatchSettleAtomicStatement memory statement, + MatchAtomicProofs memory proofs, + MatchAtomicLinkingProofs memory linkingProofs + ) = settleAtomicMatchCalldataWithMatchResult(merkleRoot, matchResult); + + // Process the match + vm.startBroadcast(externalParty.addr); + darkpool.processAtomicMatchSettle{ value: BASE_AMT }(internalPartyPayload, statement, proofs, linkingProofs); + vm.stopBroadcast(); + + // Check the token flows + FeeTake memory fees = statement.externalPartyFees; + uint256 totalFee = fees.total(); + uint256 expectedQuoteAmt = QUOTE_AMT - totalFee; + + uint256 userFinalQuoteBalance = quoteToken.balanceOf(externalParty.addr); + uint256 userFinalNativeBalance = externalParty.addr.balance; + uint256 darkpoolFinalQuoteBalance = quoteToken.balanceOf(address(darkpool)); + uint256 darkpoolFinalWethBalance = weth.balanceOf(address(darkpool)); + + assertEq(userFinalQuoteBalance, userInitialQuoteBalance + expectedQuoteAmt); + assertEq(userFinalNativeBalance, userInitialNativeBalance - BASE_AMT); + assertEq(darkpoolFinalQuoteBalance, darkpoolInitialQuoteBalance - QUOTE_AMT); + assertEq(darkpoolFinalWethBalance, darkpoolInitialWethBalance + BASE_AMT); + } + // --- Invalid Match Tests --- // /// @notice Test settling an atomic match wherein the fees exceed the receive amount @@ -409,4 +514,33 @@ contract SettleAtomicMatchTest is DarkpoolTestBase { vm.expectRevert(INVALID_PROTOCOL_FEE_REVERT_STRING); darkpool.processAtomicMatchSettle(internalPartyPayload, statement, proofs, linkingProofs); } + + /// @notice Test settling an atomic match with insufficient tx value + function test_settleAtomicMatch_insufficientTxValue() public { + Vm.Wallet memory externalParty = randomEthereumWallet(); + vm.deal(externalParty.addr, 100); + + ExternalMatchResult memory matchResult = ExternalMatchResult({ + quoteMint: address(quoteToken), + baseMint: DarkpoolConstants.NATIVE_TOKEN_ADDRESS, + quoteAmount: 100, + baseAmount: 100, + direction: ExternalMatchDirection.InternalPartyBuy + }); + + // Setup calldata + BN254.ScalarField merkleRoot = darkpool.getMerkleRoot(); + ( + PartyMatchPayload memory internalPartyPayload, + ValidMatchSettleAtomicStatement memory statement, + MatchAtomicProofs memory proofs, + MatchAtomicLinkingProofs memory linkingProofs + ) = settleAtomicMatchCalldataWithMatchResult(merkleRoot, matchResult); + + // Should fail + vm.startBroadcast(externalParty.addr); + vm.expectRevert(INVALID_ETH_DEPOSIT_AMOUNT_REVERT_STRING); + darkpool.processAtomicMatchSettle{ value: 1 wei }(internalPartyPayload, statement, proofs, linkingProofs); + vm.stopBroadcast(); + } } diff --git a/test/test-contracts/WethMock.sol b/test/test-contracts/WethMock.sol new file mode 100644 index 0000000..5ec9940 --- /dev/null +++ b/test/test-contracts/WethMock.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import { IWETH9 } from "renegade/libraries/interfaces/IWETH9.sol"; +import { ERC20Mock } from "oz-contracts/mocks/token/ERC20Mock.sol"; + +/// @title WethMock +/// @notice A mock implementation of the IWETH9 interface +contract WethMock is IWETH9, ERC20Mock { + /// @notice Deposit ETH into the contract + function deposit() external payable { + _mint(msg.sender, msg.value); + } + + /// @notice Withdraw ETH from the contract + function withdrawTo(address to, uint256 amount) external { + _burn(msg.sender, amount); + payable(to).transfer(amount); + } +}