From 896792f2a4a0ebf9517f95a76de8a2d5f59a6aa6 Mon Sep 17 00:00:00 2001 From: SanChuan <2194167956@qq.com> Date: Thu, 5 Jan 2023 12:26:02 +0000 Subject: [PATCH 01/24] add specs --- specs/bridges/nft_trading/readme.md | 46 +++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 specs/bridges/nft_trading/readme.md diff --git a/specs/bridges/nft_trading/readme.md b/specs/bridges/nft_trading/readme.md new file mode 100644 index 000000000..1e226df85 --- /dev/null +++ b/specs/bridges/nft_trading/readme.md @@ -0,0 +1,46 @@ +# Spec for NFT Trading Bridge + +## What does the bridge do? Why build it? + +This bridge enables user to trading their NFT on the established Layer 1 NFT marketplaces, aka [OpenSea](https://opensea.io/), where users can list and purchase NFTs from Aztec L2 without revealing their L1 identities. +In this way, the owner of one NFT owner is in private with the power of Aztec's zero knowledge rollup. So this bridge can enhance the secret characterist of Layer 1 user. + + +## What protocol(s) does the bridge interact with ? + + +The bridge interacts with [OpenSea](https://opensea.io/). + +## What is the flow of the bridge? +The simple flow as below: +1. User A on Aztec chain bridge interface pay and buy an NFT on Opensea in Ethereum. +2. The "Buy" order and relevant fee is send to L1 by Aztec's rollup. +3. The bridge contract in L1 is triggered to interact with OpenSea protocol. +4. Then the bridge contract own this NFT and record it to a private user id. +5. The User A On Aztec can redeem this NFT anytime, with his valid signature. + + + +### General Properties of convert(...) function + +- The bridge is synchronous, and will always return `isAsync = false`. + +- The bridge uses `_auxData` to encode the target NFT, id, price. + +- The Bridge perform token pre-approvals to allow the `ROLLUP_PROCESSOR` and `UNI_ROUTER` to pull tokens from it. + This is to reduce gas-overhead when performing the actions. It is safe to do, as the bridge is not holding the funds itself. + +## Is the contract upgradeable? + +No, the bridge is immutable without any admin role. + +## Does the bridge maintain state? + +No, the bridge doesn't maintain a state. +However, it keeps an insignificant amount of tokens (dust) in the bridge to reduce gas-costs of future transactions (in case the DUST was sent to the bridge). +By having a dust, we don't need to do a `sstore` from `0` to `non-zero`. + +## Does this bridge maintain state? If so, what is stored and why? + +Yes, this bridge maintain the NFT bought from NFT to relevant user's private identity. +This state is a record for user to redeem their NFT asset. \ No newline at end of file From 169e03a3e8a5c40bfd1c8b2b03bf9982d2b74564 Mon Sep 17 00:00:00 2001 From: SanChuan <2194167956@qq.com> Date: Thu, 5 Jan 2023 12:34:24 +0000 Subject: [PATCH 02/24] copy templete --- src/bridges/nft_trading/NftTradingBridge.sol | 584 +++++++++++++++++++ 1 file changed, 584 insertions(+) create mode 100644 src/bridges/nft_trading/NftTradingBridge.sol diff --git a/src/bridges/nft_trading/NftTradingBridge.sol b/src/bridges/nft_trading/NftTradingBridge.sol new file mode 100644 index 000000000..a87725082 --- /dev/null +++ b/src/bridges/nft_trading/NftTradingBridge.sol @@ -0,0 +1,584 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec +pragma solidity >=0.8.4; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {AztecTypes} from "rollup-encoder/libraries/AztecTypes.sol"; +import {IRollupProcessor} from "rollup-encoder/interfaces/IRollupProcessor.sol"; +import {ErrorLib} from "../base/ErrorLib.sol"; +import {BridgeBase} from "../base/BridgeBase.sol"; +import {ISwapRouter} from "../../interfaces/uniswapv3/ISwapRouter.sol"; +import {IWETH} from "../../interfaces/IWETH.sol"; +import {IQuoter} from "../../interfaces/uniswapv3/IQuoter.sol"; + +/** + * @title Aztec Connect Bridge for Trading on Opensea NFT market + * @author Sanchuan + * @notice You can use this contract to swap tokens on Uniswap v3 along complex paths. + * @dev Encoding of a path allows for up to 2 split paths (see the definition bellow) and up to 3 pools (2 middle + * tokens) in each split path. A path is encoded in _auxData parameter passed to the convert method. _auxData + * carry 64 bits of information. Along with split paths there is a minimum price encoded in auxData. + * + * Each split path takes 19 bits. Minimum price is encoded in 26 bits. Values are placed in the data as follows: + * |26 bits minimum price| |19 bits split path 2| |19 bits split path 1| + * + * Encoding of a split path is: + * |7 bits percentage| |2 bits fee| |3 bits middle token| |2 bits fee| |3 bits middle token| |2 bits fee| + * The meaning of percentage is how much of input amount will be routed through the corresponding split path. + * Fee bits are mapped to specific fee tiers as follows: + * 00 is 0.01%, 01 is 0.05%, 10 is 0.3%, 11 is 1% + * Middle tokens use the following mapping: + * 001 is ETH, 010 is USDC, 011 is USDT, 100 is DAI, 101 is WBTC, 110 is FRAX, 111 is BUSD. + * 000 means the middle token is unused. + * + * Min price is encoded as a floating point number. First 21 bits are used for significand, last 5 bits for + * exponent: |21 bits significand| |5 bits exponent| + * Minimum amount out is computed with the following formula: + * (inputValue * (significand * 10**exponent)) / (10 ** inputAssetDecimals) + * Here are 2 examples. + * 1) If I want to receive 10k Dai for 1 ETH I would set significand to 1 and exponent to 22. + * _totalInputValue = 1e18, asset = ETH (18 decimals), outputAssetA: Dai (18 decimals) + * (1e18 * (1 * 10**22)) / (10**18) = 1e22 --> 10k Dai + * 2) If I want to receive 2000 USDC for 1 ETH, I set significand to 2 and exponent to 9. + * _totalInputValue = 1e18, asset = ETH (18 decimals), outputAssetA: USDC (6 decimals) + * (1e18 * (2 * 10**9)) / (10**18) = 2e9 --> 2000 USDC + * + * Definition of split path: Split path is a term we use when there are multiple (in this case 2) paths between + * which the input amount of tokens is split. As an example we can consider swapping 100 ETH to DAI. In this case + * there could be 2 split paths. 1st split path going through ETH-USDC 500 bps fee pool and USDC-DAI 100 bps fee + * pool and 2nd split path going directly to DAI using the ETH-DAI 500 bps pool. First split path could for + * example consume 80% of input (80 ETH) and the second split path the remaining 20% (20 ETH). + */ +contract UniswapBridge is BridgeBase { + using SafeERC20 for IERC20; + + error InvalidFeeTierEncoding(); + error InvalidFeeTier(); + error InvalidTokenEncoding(); + error InvalidToken(); + error InvalidPercentageAmounts(); + error InsufficientAmountOut(); + error Overflow(); + + // @notice A struct representing a path with 2 split paths. + struct Path { + uint256 percentage1; // Percentage of input to swap through splitPath1 + bytes splitPath1; // A path encoded in a format used by Uniswap's v3 router + uint256 percentage2; // Percentage of input to swap through splitPath2 + bytes splitPath2; // A path encoded in a format used by Uniswap's v3 router + uint256 minPrice; // Minimum acceptable price + } + + struct SplitPath { + uint256 percentage; // Percentage of swap amount to send through this split path + uint256 fee1; // 1st pool fee + address token1; // Address of the 1st pool's output token + uint256 fee2; // 2nd pool fee + address token2; // Address of the 2nd pool's output token + uint256 fee3; // 3rd pool fee + } + + // @dev Event which is emitted when the output token doesn't implement decimals(). + event DefaultDecimalsWarning(); + + ISwapRouter public constant ROUTER = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); + IQuoter public constant QUOTER = IQuoter(0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6); + + // Addresses of middle tokens + address public constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + address public constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + address public constant USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; + address public constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; + address public constant WBTC = 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599; + address public constant FRAX = 0x853d955aCEf822Db058eb8505911ED77F175b99e; + address public constant BUSD = 0x4Fabb145d64652a948d72533023f6E7A623C7C53; + + uint64 public constant SPLIT_PATH_BIT_LENGTH = 19; + uint64 public constant SPLIT_PATHS_BIT_LENGTH = 38; // SPLIT_PATH_BIT_LENGTH * 2 + uint64 public constant PRICE_BIT_LENGTH = 26; // 64 - SPLIT_PATHS_BIT_LENGTH + + // @dev The following masks are used to decode 2 split paths and minimum acceptable price from 1 uint64. + // Binary number 0000000000000000000000000000000000000000000001111111111111111111 (last 19 bits) + uint64 public constant SPLIT_PATH_MASK = 0x7FFFF; + + // Binary number 0000000000000000000000000000000000000011111111111111111111111111 (last 26 bits) + uint64 public constant PRICE_MASK = 0x3FFFFFF; + + // Binary number 0000000000000000000000000000000000000000000000000000000000011111 (last 5 bits) + uint64 public constant EXPONENT_MASK = 0x1F; + + // Binary number 11 + uint64 public constant FEE_MASK = 0x3; + // Binary number 111 + uint64 public constant TOKEN_MASK = 0x7; + + /** + * @notice Set the address of rollup processor. + * @param _rollupProcessor Address of rollup processor + */ + constructor(address _rollupProcessor) BridgeBase(_rollupProcessor) {} + + // @dev Empty method which is present here in order to be able to receive ETH when unwrapping WETH. + receive() external payable {} + + /** + * @notice Sets all the important approvals. + * @param _tokensIn - An array of address of input tokens (tokens to later swap in the convert(...) function) + * @param _tokensOut - An array of address of output tokens (tokens to later return to rollup processor) + * @dev SwapBridge never holds any ERC20 tokens after or before an invocation of any of its functions. For this + * reason the following is not a security risk and makes convert(...) function more gas efficient. + */ + function preApproveTokens(address[] calldata _tokensIn, address[] calldata _tokensOut) external { + uint256 tokensLength = _tokensIn.length; + for (uint256 i; i < tokensLength;) { + address tokenIn = _tokensIn[i]; + // Using safeApprove(...) instead of approve(...) and first setting the allowance to 0 because underlying + // can be Tether + IERC20(tokenIn).safeApprove(address(ROUTER), 0); + IERC20(tokenIn).safeApprove(address(ROUTER), type(uint256).max); + unchecked { + ++i; + } + } + tokensLength = _tokensOut.length; + for (uint256 i; i < tokensLength;) { + address tokenOut = _tokensOut[i]; + // Using safeApprove(...) instead of approve(...) and first setting the allowance to 0 because underlying + // can be Tether + IERC20(tokenOut).safeApprove(address(ROLLUP_PROCESSOR), 0); + IERC20(tokenOut).safeApprove(address(ROLLUP_PROCESSOR), type(uint256).max); + unchecked { + ++i; + } + } + } + + /** + * @notice Registers subsidy criteria for a given token pair. + * @param _tokenIn - Input token to swap + * @param _tokenOut - Output token to swap + */ + function registerSubsidyCriteria(address _tokenIn, address _tokenOut) external { + SUBSIDY.setGasUsageAndMinGasPerMinute({ + _criteria: _computeCriteria(_tokenIn, _tokenOut), + _gasUsage: uint32(300000), // 300k gas (Note: this is a gas usage when only 1 split path is used) + _minGasPerMinute: uint32(100) // 1 fully subsidized call per 2 days (300k / (24 * 60) / 2) + }); + } + + /** + * @notice A function which swaps input token for output token along the path encoded in _auxData. + * @param _inputAssetA - Input ERC20 token + * @param _outputAssetA - Output ERC20 token + * @param _totalInputValue - Amount of input token to swap + * @param _interactionNonce - Interaction nonce + * @param _auxData - Encoded path (gets decoded to Path struct) + * @param _rollupBeneficiary - Address which receives subsidy if the call is eligible for it + * @return outputValueA - The amount of output token received + */ + function convert( + AztecTypes.AztecAsset calldata _inputAssetA, + AztecTypes.AztecAsset calldata, + AztecTypes.AztecAsset calldata _outputAssetA, + AztecTypes.AztecAsset calldata, + uint256 _totalInputValue, + uint256 _interactionNonce, + uint64 _auxData, + address _rollupBeneficiary + ) external payable override (BridgeBase) onlyRollup returns (uint256 outputValueA, uint256, bool) { + // Accumulate subsidy to _rollupBeneficiary + SUBSIDY.claimSubsidy( + _computeCriteria(_inputAssetA.erc20Address, _outputAssetA.erc20Address), _rollupBeneficiary + ); + + bool inputIsEth = _inputAssetA.assetType == AztecTypes.AztecAssetType.ETH; + bool outputIsEth = _outputAssetA.assetType == AztecTypes.AztecAssetType.ETH; + + if (_inputAssetA.assetType != AztecTypes.AztecAssetType.ERC20 && !inputIsEth) { + revert ErrorLib.InvalidInputA(); + } + if (_outputAssetA.assetType != AztecTypes.AztecAssetType.ERC20 && !outputIsEth) { + revert ErrorLib.InvalidOutputA(); + } + + Path memory path = _decodePath( + inputIsEth ? WETH : _inputAssetA.erc20Address, _auxData, outputIsEth ? WETH : _outputAssetA.erc20Address + ); + + uint256 inputValueSplitPath1 = (_totalInputValue * path.percentage1) / 100; + + if (path.percentage1 != 0) { + // Swap using the first swap path + outputValueA = ROUTER.exactInput{value: inputIsEth ? inputValueSplitPath1 : 0}( + ISwapRouter.ExactInputParams({ + path: path.splitPath1, + recipient: address(this), + deadline: block.timestamp, + amountIn: inputValueSplitPath1, + amountOutMinimum: 0 + }) + ); + } + + if (path.percentage2 != 0) { + // Swap using the second swap path + uint256 inputValueSplitPath2 = _totalInputValue - inputValueSplitPath1; + outputValueA += ROUTER.exactInput{value: inputIsEth ? inputValueSplitPath2 : 0}( + ISwapRouter.ExactInputParams({ + path: path.splitPath2, + recipient: address(this), + deadline: block.timestamp, + amountIn: inputValueSplitPath2, + amountOutMinimum: 0 + }) + ); + } + + uint256 tokenInDecimals = 18; + if (!inputIsEth) { + try IERC20Metadata(_inputAssetA.erc20Address).decimals() returns (uint8 decimals) { + tokenInDecimals = decimals; + } catch (bytes memory) { + emit DefaultDecimalsWarning(); + } + } + uint256 amountOutMinimum = (_totalInputValue * path.minPrice) / 10 ** tokenInDecimals; + if (outputValueA < amountOutMinimum) revert InsufficientAmountOut(); + + if (outputIsEth) { + IWETH(WETH).withdraw(outputValueA); + IRollupProcessor(ROLLUP_PROCESSOR).receiveEthFromBridge{value: outputValueA}(_interactionNonce); + } + } + + /** + * @notice A function which encodes path to a format expected in _auxData of this.convert(...) + * @param _amountIn - Amount of tokenIn to swap + * @param _minAmountOut - Amount of tokenOut to receive + * @param _tokenIn - Address of _tokenIn (@dev used only to fetch decimals) + * @param _splitPath1 - Split path to encode + * @param _splitPath2 - Split path to encode + * @return Path encoded in a format expected in _auxData of this.convert(...) + * @dev This function is not optimized and is expected to be used on frontend and in tests. + * @dev Reverts when min price is bigger than max encodeable value. + */ + function encodePath( + uint256 _amountIn, + uint256 _minAmountOut, + address _tokenIn, + SplitPath calldata _splitPath1, + SplitPath calldata _splitPath2 + ) external view returns (uint64) { + if (_splitPath1.percentage + _splitPath2.percentage != 100) revert InvalidPercentageAmounts(); + + return uint64( + ( + _computeEncodedMinPrice(_amountIn, _minAmountOut, IERC20Metadata(_tokenIn).decimals()) + << SPLIT_PATHS_BIT_LENGTH + ) + (_encodeSplitPath(_splitPath1) << SPLIT_PATH_BIT_LENGTH) + _encodeSplitPath(_splitPath2) + ); + } + + /** + * @notice A function which encodes path to a format expected in _auxData of this.convert(...) + * @param _amountIn - Amount of tokenIn to swap + * @param _tokenIn - Address of _tokenIn (@dev used only to fetch decimals) + * @param _path - Split path to encode + * @param _tokenOut - Address of _tokenIn (@dev used only to fetch decimals) + * @return amountOut - + */ + function quote(uint256 _amountIn, address _tokenIn, uint64 _path, address _tokenOut) + external + returns (uint256 amountOut) + { + Path memory path = _decodePath(_tokenIn, _path, _tokenOut); + uint256 inputValueSplitPath1 = (_amountIn * path.percentage1) / 100; + + if (path.percentage1 != 0) { + // Swap using the first swap path + amountOut += QUOTER.quoteExactInput(path.splitPath1, inputValueSplitPath1); + } + + if (path.percentage2 != 0) { + // Swap using the second swap path + amountOut += QUOTER.quoteExactInput(path.splitPath2, _amountIn - inputValueSplitPath1); + } + } + + /** + * @notice Computes the criteria that is passed when claiming subsidy. + * @param _inputAssetA The input asset + * @param _outputAssetA The output asset + * @return The criteria + */ + function computeCriteria( + AztecTypes.AztecAsset calldata _inputAssetA, + AztecTypes.AztecAsset calldata, + AztecTypes.AztecAsset calldata _outputAssetA, + AztecTypes.AztecAsset calldata, + uint64 + ) public pure override (BridgeBase) returns (uint256) { + return _computeCriteria(_inputAssetA.erc20Address, _outputAssetA.erc20Address); + } + + function _computeCriteria(address _inputToken, address _outputToken) internal pure returns (uint256) { + return uint256(keccak256(abi.encodePacked(_inputToken, _outputToken))); + } + + /** + * @notice A function which computes min price and encodes it in the format used in this bridge. + * @param _amountIn - Amount of tokenIn to swap + * @param _minAmountOut - Amount of tokenOut to receive + * @param _tokenInDecimals - Number of decimals of tokenIn + * @return encodedMinPrice - Min acceptable encoded in a format used in this bridge. + * @dev This function is not optimized and is expected to be used on frontend and in tests. + * @dev Reverts when min price is bigger than max encodeable value. + */ + function _computeEncodedMinPrice(uint256 _amountIn, uint256 _minAmountOut, uint256 _tokenInDecimals) + internal + pure + returns (uint256 encodedMinPrice) + { + uint256 minPrice = (_minAmountOut * 10 ** _tokenInDecimals) / _amountIn; + // 2097151 = 2**21 - 1 --> this number and its multiples of 10 can be encoded without precision loss + if (minPrice <= 2097151) { + // minPrice is smaller than the boundary of significand --> significand = _x, exponent = 0 + encodedMinPrice = minPrice << 5; + } else { + uint256 exponent = 0; + while (minPrice > 2097151) { + minPrice /= 10; + ++exponent; + // 31 = 2**5 - 1 --> max exponent + if (exponent > 31) revert Overflow(); + } + encodedMinPrice = (minPrice << 5) + exponent; + } + } + + /** + * @notice A function which encodes a split path. + * @param _path - Split path to encode + * @return Encoded split path (in the last 19 bits of uint256) + * @dev In place of unused middle tokens leave address(0). + * @dev Fee tier corresponding to unused middle token is ignored. + */ + function _encodeSplitPath(SplitPath calldata _path) internal pure returns (uint256) { + if (_path.percentage == 0) return 0; + return (_path.percentage << 12) + (_encodeFeeTier(_path.fee1) << 10) + (_encodeMiddleToken(_path.token1) << 7) + + (_encodeFeeTier(_path.fee2) << 5) + (_encodeMiddleToken(_path.token2) << 2) + (_encodeFeeTier(_path.fee3)); + } + + /** + * @notice A function which encodes fee tier. + * @param _feeTier - Fee tier in bps + * @return Encoded fee tier (in the last 2 bits of uint256) + */ + function _encodeFeeTier(uint256 _feeTier) internal pure returns (uint256) { + if (_feeTier == 100) { + // Binary number 00 + return 0; + } + if (_feeTier == 500) { + // Binary number 01 + return 1; + } + if (_feeTier == 3000) { + // Binary number 10 + return 2; + } + if (_feeTier == 10000) { + // Binary number 11 + return 3; + } + revert InvalidFeeTier(); + } + + /** + * @notice A function which returns token encoding for a given token address. + * @param _token - Token address + * @return encodedToken - Encoded token (in the last 3 bits of uint256) + */ + function _encodeMiddleToken(address _token) internal pure returns (uint256 encodedToken) { + if (_token == address(0)) { + // unused token + return 0; + } + if (_token == WETH) { + // binary number 001 + return 1; + } + if (_token == USDC) { + // binary number 010 + return 2; + } + if (_token == USDT) { + // binary number 011 + return 3; + } + if (_token == DAI) { + // binary number 100 + return 4; + } + if (_token == WBTC) { + // binary number 101 + return 5; + } + if (_token == FRAX) { + // binary number 110 + return 6; + } + if (_token == BUSD) { + // binary number 111 + return 7; + } + revert InvalidToken(); + } + + /** + * @notice A function which deserializes encoded path to Path struct. + * @param _tokenIn - Input ERC20 token + * @param _encodedPath - Encoded path + * @param _tokenOut - Output ERC20 token + * @return path - Decoded/deserialized path struct + */ + function _decodePath(address _tokenIn, uint256 _encodedPath, address _tokenOut) + internal + pure + returns (Path memory path) + { + (uint256 percentage1, bytes memory splitPath1) = + _decodeSplitPath(_tokenIn, _encodedPath & SPLIT_PATH_MASK, _tokenOut); + path.percentage1 = percentage1; + path.splitPath1 = splitPath1; + + (uint256 percentage2, bytes memory splitPath2) = + _decodeSplitPath(_tokenIn, (_encodedPath >> SPLIT_PATH_BIT_LENGTH) & SPLIT_PATH_MASK, _tokenOut); + + if (percentage1 + percentage2 != 100) revert InvalidPercentageAmounts(); + + path.percentage2 = percentage2; + path.splitPath2 = splitPath2; + path.minPrice = _decodeMinPrice(_encodedPath >> SPLIT_PATHS_BIT_LENGTH); + } + + /** + * @notice A function which returns a percentage of input going through the split path and the split path encoded + * in a format compatible with Uniswap router. + * @param _tokenIn - Input ERC20 token + * @param _encodedSplitPath - Encoded split path (in the last 19 bits of uint256) + * @param _tokenOut - Output ERC20 token + * @return percentage - A percentage of input going through the corresponding split path + * @return splitPath - A split path encoded in a format compatible with Uniswap router + */ + function _decodeSplitPath(address _tokenIn, uint256 _encodedSplitPath, address _tokenOut) + internal + pure + returns (uint256 percentage, bytes memory splitPath) + { + uint256 fee3 = _encodedSplitPath & FEE_MASK; + uint256 middleToken2 = (_encodedSplitPath >> 2) & TOKEN_MASK; + uint256 fee2 = (_encodedSplitPath >> 5) & FEE_MASK; + uint256 middleToken1 = (_encodedSplitPath >> 7) & TOKEN_MASK; + uint256 fee1 = (_encodedSplitPath >> 10) & FEE_MASK; + percentage = _encodedSplitPath >> 12; + + if (middleToken1 != 0 && middleToken2 != 0) { + splitPath = abi.encodePacked( + _tokenIn, + _decodeFeeTier(fee1), + _decodeMiddleToken(middleToken1), + _decodeFeeTier(fee2), + _decodeMiddleToken(middleToken2), + _decodeFeeTier(fee3), + _tokenOut + ); + } else if (middleToken1 != 0) { + splitPath = abi.encodePacked( + _tokenIn, _decodeFeeTier(fee1), _decodeMiddleToken(middleToken1), _decodeFeeTier(fee3), _tokenOut + ); + } else if (middleToken2 != 0) { + splitPath = abi.encodePacked( + _tokenIn, _decodeFeeTier(fee2), _decodeMiddleToken(middleToken2), _decodeFeeTier(fee3), _tokenOut + ); + } else { + splitPath = abi.encodePacked(_tokenIn, _decodeFeeTier(fee3), _tokenOut); + } + } + + /** + * @notice A function which converts minimum price in a floating point format to integer. + * @param _encodedMinPrice - Encoded minimum price (in the last 26 bits of uint256) + * @return minPrice - Minimum acceptable price represented as an integer + */ + function _decodeMinPrice(uint256 _encodedMinPrice) internal pure returns (uint256 minPrice) { + // 21 bits significand, 5 bits exponent + uint256 significand = _encodedMinPrice >> 5; + uint256 exponent = _encodedMinPrice & EXPONENT_MASK; + minPrice = significand * 10 ** exponent; + } + + /** + * @notice A function which converts encoded fee tier to a fee tier in an integer format. + * @param _encodedFeeTier - Encoded fee tier (in the last 2 bits of uint256) + * @return feeTier - Decoded fee tier in an integer format + */ + function _decodeFeeTier(uint256 _encodedFeeTier) internal pure returns (uint24 feeTier) { + if (_encodedFeeTier == 0) { + // Binary number 00 + return uint24(100); + } + if (_encodedFeeTier == 1) { + // Binary number 01 + return uint24(500); + } + if (_encodedFeeTier == 2) { + // Binary number 10 + return uint24(3000); + } + if (_encodedFeeTier == 3) { + // Binary number 11 + return uint24(10000); + } + revert InvalidFeeTierEncoding(); + } + + /** + * @notice A function which returns token address for an encoded token. + * @param _encodedToken - Encoded token (in the last 3 bits of uint256) + * @return token - Token address + */ + function _decodeMiddleToken(uint256 _encodedToken) internal pure returns (address token) { + if (_encodedToken == 1) { + // binary number 001 + return WETH; + } + if (_encodedToken == 2) { + // binary number 010 + return USDC; + } + if (_encodedToken == 3) { + // binary number 011 + return USDT; + } + if (_encodedToken == 4) { + // binary number 100 + return DAI; + } + if (_encodedToken == 5) { + // binary number 101 + return WBTC; + } + if (_encodedToken == 6) { + // binary number 110 + return FRAX; + } + if (_encodedToken == 7) { + // binary number 111 + return BUSD; + } + revert InvalidTokenEncoding(); + } +} From caab3a2c9ab49c3edcc74b67064f46502a54f37e Mon Sep 17 00:00:00 2001 From: SanChuan <2194167956@qq.com> Date: Thu, 5 Jan 2023 16:07:13 +0000 Subject: [PATCH 03/24] learn test --- src/test/bridges/nft_trading/ExampleE2E.t.sol | 109 ++++++++++++++++ .../bridges/nft_trading/ExampleUnit.t.sol | 119 ++++++++++++++++++ 2 files changed, 228 insertions(+) create mode 100644 src/test/bridges/nft_trading/ExampleE2E.t.sol create mode 100644 src/test/bridges/nft_trading/ExampleUnit.t.sol diff --git a/src/test/bridges/nft_trading/ExampleE2E.t.sol b/src/test/bridges/nft_trading/ExampleE2E.t.sol new file mode 100644 index 000000000..9f64f7f84 --- /dev/null +++ b/src/test/bridges/nft_trading/ExampleE2E.t.sol @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec. +pragma solidity >=0.8.4; + +import {BridgeTestBase} from "./../../aztec/base/BridgeTestBase.sol"; +import {AztecTypes} from "rollup-encoder/libraries/AztecTypes.sol"; + +// Example-specific imports +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ExampleBridge} from "../../../bridges/example/ExampleBridge.sol"; +import {ErrorLib} from "../../../bridges/base/ErrorLib.sol"; + +/** + * @notice The purpose of this test is to test the bridge in an environment that is as close to the final deployment + * as possible without spinning up all the rollup infrastructure (sequencer, proof generator etc.). + */ +contract ExampleE2ETest is BridgeTestBase { + address public constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + address private constant BENEFICIARY = address(11); + + // The reference to the example bridge + ExampleBridge internal bridge; + // To store the id of the example bridge after being added + uint256 private id; + + function setUp() public { + // Deploy a new example bridge + bridge = new ExampleBridge(address(ROLLUP_PROCESSOR)); + + // Use the label cheatcode to mark the address with "Example Bridge" in the traces + vm.label(address(bridge), "Example Bridge"); + + // Impersonate the multi-sig to add a new bridge + vm.startPrank(MULTI_SIG); + + // List the example-bridge with a gasLimit of 120k + // WARNING: If you set this value too low the interaction will fail for seemingly no reason! + // OTOH if you se it too high bridge users will pay too much + ROLLUP_PROCESSOR.setSupportedBridge(address(bridge), 120000); + + // List USDC with a gasLimit of 100k + // Note: necessary for assets which are not already registered on RollupProcessor + // Call https://etherscan.io/address/0xFF1F2B4ADb9dF6FC8eAFecDcbF96A2B351680455#readProxyContract#F25 to get + // addresses of all the listed ERC20 tokens + ROLLUP_PROCESSOR.setSupportedAsset(USDC, 100000); + + vm.stopPrank(); + + // Fetch the id of the example bridge + id = ROLLUP_PROCESSOR.getSupportedBridgesLength(); + + // Subsidize the bridge when used with USDC and register a beneficiary + AztecTypes.AztecAsset memory usdcAsset = ROLLUP_ENCODER.getRealAztecAsset(USDC); + uint256 criteria = bridge.computeCriteria(usdcAsset, emptyAsset, usdcAsset, emptyAsset, 0); + uint32 gasPerMinute = 200; + SUBSIDY.subsidize{value: 1 ether}(address(bridge), criteria, gasPerMinute); + + SUBSIDY.registerBeneficiary(BENEFICIARY); + + // Set the rollupBeneficiary on BridgeTestBase so that it gets included in the proofData + ROLLUP_ENCODER.setRollupBeneficiary(BENEFICIARY); + } + + // @dev In order to avoid overflows we set _depositAmount to be uint96 instead of uint256. + function testExampleBridgeE2ETest(uint96 _depositAmount) public { + vm.assume(_depositAmount > 1); + vm.warp(block.timestamp + 1 days); + + // Use the helper function to fetch the support AztecAsset for DAI + AztecTypes.AztecAsset memory usdcAsset = ROLLUP_ENCODER.getRealAztecAsset(address(USDC)); + + // Mint the depositAmount of Dai to rollupProcessor + deal(USDC, address(ROLLUP_PROCESSOR), _depositAmount); + + // Computes the encoded data for the specific bridge interaction + ROLLUP_ENCODER.defiInteractionL2(id, usdcAsset, emptyAsset, usdcAsset, emptyAsset, 0, _depositAmount); + + // Execute the rollup with the bridge interaction. Ensure that event as seen above is emitted. + (uint256 outputValueA, uint256 outputValueB, bool isAsync) = ROLLUP_ENCODER.processRollupAndGetBridgeResult(); + + // Note: Unlike in unit tests there is no need to manually transfer the tokens - RollupProcessor does this + + // Check the output values are as expected + assertEq(outputValueA, _depositAmount, "outputValueA doesn't equal deposit"); + assertEq(outputValueB, 0, "Non-zero outputValueB"); + assertFalse(isAsync, "Bridge is not synchronous"); + + // Check that the balance of the rollup is same as before interaction (bridge just sends funds back) + assertEq(_depositAmount, IERC20(USDC).balanceOf(address(ROLLUP_PROCESSOR)), "Balances must match"); + + // Perform a second rollup with half the deposit, perform similar checks. + uint256 secondDeposit = _depositAmount / 2; + + ROLLUP_ENCODER.defiInteractionL2(id, usdcAsset, emptyAsset, usdcAsset, emptyAsset, 0, secondDeposit); + + // Execute the rollup with the bridge interaction. Ensure that event as seen above is emitted. + (outputValueA, outputValueB, isAsync) = ROLLUP_ENCODER.processRollupAndGetBridgeResult(); + + // Check the output values are as expected + assertEq(outputValueA, secondDeposit, "outputValueA doesn't equal second deposit"); + assertEq(outputValueB, 0, "Non-zero outputValueB"); + assertFalse(isAsync, "Bridge is not synchronous"); + + // Check that the balance of the rollup is same as before interaction (bridge just sends funds back) + assertEq(_depositAmount, IERC20(USDC).balanceOf(address(ROLLUP_PROCESSOR)), "Balances must match"); + + assertGt(SUBSIDY.claimableAmount(BENEFICIARY), 0, "Claimable was not updated"); + } +} diff --git a/src/test/bridges/nft_trading/ExampleUnit.t.sol b/src/test/bridges/nft_trading/ExampleUnit.t.sol new file mode 100644 index 000000000..0c6aeb464 --- /dev/null +++ b/src/test/bridges/nft_trading/ExampleUnit.t.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec. +pragma solidity >=0.8.4; + +import {BridgeTestBase} from "./../../aztec/base/BridgeTestBase.sol"; +import {AztecTypes} from "rollup-encoder/libraries/AztecTypes.sol"; + +// Example-specific imports +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ExampleBridge} from "../../../bridges/example/ExampleBridge.sol"; +import {ErrorLib} from "../../../bridges/base/ErrorLib.sol"; + +// @notice The purpose of this test is to directly test convert functionality of the bridge. +contract ExampleUnitTest is BridgeTestBase { + address private constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; + address private constant BENEFICIARY = address(11); + + address private rollupProcessor; + // The reference to the example bridge + ExampleBridge private bridge; + + // @dev This method exists on RollupProcessor.sol. It's defined here in order to be able to receive ETH like a real + // rollup processor would. + function receiveEthFromBridge(uint256 _interactionNonce) external payable {} + + function setUp() public { + // In unit tests we set address of rollupProcessor to the address of this test contract + rollupProcessor = address(this); + + // Deploy a new example bridge + bridge = new ExampleBridge(rollupProcessor); + + // Set ETH balance of bridge and BENEFICIARY to 0 for clarity (somebody sent ETH to that address on mainnet) + vm.deal(address(bridge), 0); + vm.deal(BENEFICIARY, 0); + + // Use the label cheatcode to mark the address with "Example Bridge" in the traces + vm.label(address(bridge), "Example Bridge"); + + // Subsidize the bridge when used with Dai and register a beneficiary + AztecTypes.AztecAsset memory daiAsset = ROLLUP_ENCODER.getRealAztecAsset(DAI); + uint256 criteria = bridge.computeCriteria(daiAsset, emptyAsset, daiAsset, emptyAsset, 0); + uint32 gasPerMinute = 200; + SUBSIDY.subsidize{value: 1 ether}(address(bridge), criteria, gasPerMinute); + + SUBSIDY.registerBeneficiary(BENEFICIARY); + } + + function testInvalidCaller(address _callerAddress) public { + vm.assume(_callerAddress != rollupProcessor); + // Use HEVM cheatcode to call from a different address than is address(this) + vm.prank(_callerAddress); + vm.expectRevert(ErrorLib.InvalidCaller.selector); + bridge.convert(emptyAsset, emptyAsset, emptyAsset, emptyAsset, 0, 0, 0, address(0)); + } + + function testInvalidInputAssetType() public { + vm.expectRevert(ErrorLib.InvalidInputA.selector); + bridge.convert(emptyAsset, emptyAsset, emptyAsset, emptyAsset, 0, 0, 0, address(0)); + } + + function testInvalidOutputAssetType() public { + AztecTypes.AztecAsset memory inputAssetA = + AztecTypes.AztecAsset({id: 1, erc20Address: DAI, assetType: AztecTypes.AztecAssetType.ERC20}); + vm.expectRevert(ErrorLib.InvalidOutputA.selector); + bridge.convert(inputAssetA, emptyAsset, emptyAsset, emptyAsset, 0, 0, 0, address(0)); + } + + function testExampleBridgeUnitTestFixed() public { + testExampleBridgeUnitTest(10 ether); + } + + // @notice The purpose of this test is to directly test convert functionality of the bridge. + // @dev In order to avoid overflows we set _depositAmount to be uint96 instead of uint256. + function testExampleBridgeUnitTest(uint96 _depositAmount) public { + vm.warp(block.timestamp + 1 days); + + // Define input and output assets + AztecTypes.AztecAsset memory inputAssetA = + AztecTypes.AztecAsset({id: 1, erc20Address: DAI, assetType: AztecTypes.AztecAssetType.ERC20}); + + AztecTypes.AztecAsset memory outputAssetA = inputAssetA; + + // Rollup processor transfers ERC20 tokens to the bridge before calling convert. Since we are calling + // bridge.convert(...) function directly we have to transfer the funds in the test on our own. In this case + // we'll solve it by directly minting the _depositAmount of Dai to the bridge. + deal(DAI, address(bridge), _depositAmount); + + // Store dai balance before interaction to be able to verify the balance after interaction is correct + uint256 daiBalanceBefore = IERC20(DAI).balanceOf(rollupProcessor); + + (uint256 outputValueA, uint256 outputValueB, bool isAsync) = bridge.convert( + inputAssetA, // _inputAssetA - definition of an input asset + emptyAsset, // _inputAssetB - not used so can be left empty + outputAssetA, // _outputAssetA - in this example equal to input asset + emptyAsset, // _outputAssetB - not used so can be left empty + _depositAmount, // _totalInputValue - an amount of input asset A sent to the bridge + 0, // _interactionNonce + 0, // _auxData - not used in the example bridge + BENEFICIARY // _rollupBeneficiary - address, the subsidy will be sent to + ); + + // Now we transfer the funds back from the bridge to the rollup processor + // In this case input asset equals output asset so I only work with the input asset definition + // Basically in all the real world use-cases output assets would differ from input assets + IERC20(inputAssetA.erc20Address).transferFrom(address(bridge), rollupProcessor, outputValueA); + + assertEq(outputValueA, _depositAmount, "Output value A doesn't equal deposit amount"); + assertEq(outputValueB, 0, "Output value B is not 0"); + assertTrue(!isAsync, "Bridge is incorrectly in an async mode"); + + uint256 daiBalanceAfter = IERC20(DAI).balanceOf(rollupProcessor); + + assertEq(daiBalanceAfter - daiBalanceBefore, _depositAmount, "Balances must match"); + + SUBSIDY.withdraw(BENEFICIARY); + assertGt(BENEFICIARY.balance, 0, "Subsidy was not claimed"); + } +} From a26f1bcacb4043715f6a431f0c478ed307cc641e Mon Sep 17 00:00:00 2001 From: SanChuan <2194167956@qq.com> Date: Fri, 6 Jan 2023 03:20:57 +0000 Subject: [PATCH 04/24] update specs flow --- specs/bridges/nft_trading/readme.md | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/specs/bridges/nft_trading/readme.md b/specs/bridges/nft_trading/readme.md index 1e226df85..85d463c89 100644 --- a/specs/bridges/nft_trading/readme.md +++ b/specs/bridges/nft_trading/readme.md @@ -7,28 +7,36 @@ In this way, the owner of one NFT owner is in private with the power of Aztec's ## What protocol(s) does the bridge interact with ? - - The bridge interacts with [OpenSea](https://opensea.io/). ## What is the flow of the bridge? -The simple flow as below: + +The NFT bridge will design as a Wrap Erc721 Contract, that means everytime when the contract received a NFT, it will mint a relevant Wrapped NFT to this user, This Wrapped NFT can be transfer to Aztec L2, also can be unwrap and redeem the original NFT anytimes. + +The `BUY` flow as below: 1. User A on Aztec chain bridge interface pay and buy an NFT on Opensea in Ethereum. 2. The "Buy" order and relevant fee is send to L1 by Aztec's rollup. 3. The bridge contract in L1 is triggered to interact with OpenSea protocol. -4. Then the bridge contract own this NFT and record it to a private user id. -5. The User A On Aztec can redeem this NFT anytime, with his valid signature. - +4. Then the bridge contract own this NFT and Mint a Wrapped NFT for Aztec L2 to bridge. +5. The User A On Aztec Owned this Wrapped NFT. +The `REDEEM` flow as below: +1. User A hold an wrapped NFT in Aztec L2. +2. He call the bridge to unwrapped and send the real NFT to a L1 address. +3. The Bridge contract is trigged, and send the resl NFT to the user specifed address, and burn the wrapped NFT. ### General Properties of convert(...) function + The `AztecTypes.AztecAsset calldata _inputAssetA` should be specificed as the Bridge's wrapped NFT. + And the relevant function calling is encoded into the `_auxData` Info. + In the Bridge contract, the function will be routed by the decoded +`_auxData`. - The bridge is synchronous, and will always return `isAsync = false`. - The bridge uses `_auxData` to encode the target NFT, id, price. -- The Bridge perform token pre-approvals to allow the `ROLLUP_PROCESSOR` and `UNI_ROUTER` to pull tokens from it. - This is to reduce gas-overhead when performing the actions. It is safe to do, as the bridge is not holding the funds itself. +- The Bridge perform token pre-approvals to allow the `ROLLUP_PROCESSOR` to pull tokens from it. + ## Is the contract upgradeable? From 1e7e3a52217a171f9a03ce034ed9348bc03fa762 Mon Sep 17 00:00:00 2001 From: sc <2194167956@qq.com> Date: Fri, 6 Jan 2023 22:45:49 +0800 Subject: [PATCH 05/24] add NFT Transfer templete Refers to: https://github.com/critesjosh/aztec-connect-starter/tree/nft-bridge Kudo to this guy. --- .gitignore | 3 +- src/bridges/nft-basic/NFTVault.sol | 135 +++++++++ src/bridges/nft_trading/NFTVault.sol | 135 +++++++++ src/bridges/registry/AddressRegistry.sol | 87 ++++++ .../nft-basic/NFTVaultDeployment.s.sol | 42 +++ .../registry/AddressRegistryDeployment.s.sol | 28 ++ .../bridges/nft-basic/NFTVaultBasicE2E.t.sol | 157 +++++++++++ .../bridges/nft-basic/NFTVaultBasicUnit.t.sol | 257 ++++++++++++++++++ .../bridges/registry/AddressRegistryE2E.t.sol | 76 ++++++ .../registry/AddressRegistryUnitTest.t.sol | 127 +++++++++ 10 files changed, 1046 insertions(+), 1 deletion(-) create mode 100644 src/bridges/nft-basic/NFTVault.sol create mode 100644 src/bridges/nft_trading/NFTVault.sol create mode 100644 src/bridges/registry/AddressRegistry.sol create mode 100644 src/deployment/nft-basic/NFTVaultDeployment.s.sol create mode 100644 src/deployment/registry/AddressRegistryDeployment.s.sol create mode 100644 src/test/bridges/nft-basic/NFTVaultBasicE2E.t.sol create mode 100644 src/test/bridges/nft-basic/NFTVaultBasicUnit.t.sol create mode 100644 src/test/bridges/registry/AddressRegistryE2E.t.sol create mode 100644 src/test/bridges/registry/AddressRegistryUnitTest.t.sol diff --git a/.gitignore b/.gitignore index 4b6e85fbc..3d60e9478 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ out/ node_modules/ yarn-error.log typechain-types/ -broadcast/ \ No newline at end of file +broadcast/ +compose-dev.yaml diff --git a/src/bridges/nft-basic/NFTVault.sol b/src/bridges/nft-basic/NFTVault.sol new file mode 100644 index 000000000..dec468119 --- /dev/null +++ b/src/bridges/nft-basic/NFTVault.sol @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec. +pragma solidity >=0.8.4; + +import {IERC721} from "../../../lib/openzeppelin-contracts/contracts/interfaces/IERC721.sol"; +import {AztecTypes} from "../../../lib/rollup-encoder/src/libraries/AztecTypes.sol"; +import {ErrorLib} from "../base/ErrorLib.sol"; +import {BridgeBase} from "../base/BridgeBase.sol"; +import {AddressRegistry} from "../registry/AddressRegistry.sol"; + +/** + * @title Basic NFT Vault for Aztec. + * @author Josh Crites, (@critesjosh on Github), Aztec Team + * @notice You can use this contract to hold your NFTs on Aztec. Whoever holds the corresponding virutal asset note can withdraw the NFT. + * @dev This bridge demonstrates basic functionality for an NFT bridge. This may be extended to support more features. + */ +contract NFTVault is BridgeBase { + struct NFTAsset { + address collection; + uint256 tokenId; + } + + AddressRegistry public immutable REGISTRY; + + mapping(uint256 => NFTAsset) public nftAssets; + + error InvalidVirtualAssetId(); + + event NFTDeposit(uint256 indexed virtualAssetId, address indexed collection, uint256 indexed tokenId); + event NFTWithdraw(uint256 indexed virtualAssetId, address indexed collection, uint256 indexed tokenId); + + /** + * @notice Set the addresses of RollupProcessor and AddressRegistry + * @param _rollupProcessor Address of the RollupProcessor + * @param _registry Address of the AddressRegistry + */ + constructor(address _rollupProcessor, address _registry) BridgeBase(_rollupProcessor) { + REGISTRY = AddressRegistry(_registry); + } + + /** + * @notice Function for the first step of a NFT deposit, a NFT withdrawal, or transfer to another NFTVault. + * @dev This method can only be called from the RollupProcessor. The first step of the + * deposit flow returns a virutal asset note that will represent the NFT on Aztec. After the + * virutal asset note is received on Aztec, the user calls matchAndPull which deposits the NFT + * into Aztec and matches it with the virtual asset. When the virutal asset is sent to this function + * it is burned and the NFT is sent to the recipient passed in _auxData. + * + * @param _inputAssetA - ETH (Deposit) or VIRTUAL (Withdrawal) + * @param _outputAssetA - VIRTUAL (Deposit) or 0 ETH (Withdrawal) + * @param _totalInputValue - must be 1 wei (Deposit) or 1 VIRTUAL (Withdrawal) + * @param _interactionNonce - A globally unique identifier of this interaction/`convert(...)` call + * corresponding to the returned virtual asset id + * @param _auxData - corresponds to the Ethereum address id in the AddressRegistry.sol for withdrawals + * @return outputValueA - 1 VIRTUAL asset (Deposit) or 0 ETH (Withdrawal) + * + */ + + function convert( + AztecTypes.AztecAsset calldata _inputAssetA, + AztecTypes.AztecAsset calldata, + AztecTypes.AztecAsset calldata _outputAssetA, + AztecTypes.AztecAsset calldata, + uint256 _totalInputValue, + uint256 _interactionNonce, + uint64 _auxData, + address + ) + external + payable + override (BridgeBase) + onlyRollup + returns (uint256 outputValueA, uint256 outputValueB, bool isAsync) + { + if ( + _inputAssetA.assetType == AztecTypes.AztecAssetType.NOT_USED + || _inputAssetA.assetType == AztecTypes.AztecAssetType.ERC20 + ) revert ErrorLib.InvalidInputA(); + if ( + _outputAssetA.assetType == AztecTypes.AztecAssetType.NOT_USED + || _outputAssetA.assetType == AztecTypes.AztecAssetType.ERC20 + ) revert ErrorLib.InvalidOutputA(); + if (_totalInputValue != 1) { + revert ErrorLib.InvalidInputAmount(); + } + if ( + _inputAssetA.assetType == AztecTypes.AztecAssetType.ETH + && _outputAssetA.assetType == AztecTypes.AztecAssetType.VIRTUAL + ) { + return (1, 0, false); + } else if (_inputAssetA.assetType == AztecTypes.AztecAssetType.VIRTUAL) { + NFTAsset memory token = nftAssets[_inputAssetA.id]; + if (token.collection == address(0x0)) { + revert ErrorLib.InvalidInputA(); + } + + address to = REGISTRY.addresses(_auxData); + if (to == address(0x0)) { + revert ErrorLib.InvalidAuxData(); + } + delete nftAssets[_inputAssetA.id]; + emit NFTWithdraw(_inputAssetA.id, token.collection, token.tokenId); + + if (_outputAssetA.assetType == AztecTypes.AztecAssetType.ETH) { + IERC721(token.collection).transferFrom(address(this), to, token.tokenId); + return (0, 0, false); + } else { + IERC721(token.collection).approve(to, token.tokenId); + NFTVault(to).matchAndPull(_interactionNonce, token.collection, token.tokenId); + return (1, 0, false); + } + } + } + + /** + * @notice Function for the second step of a NFT deposit or for transfers from other NFTVaults. + * @dev For a deposit, this method is called by an Ethereum L1 account that owns the NFT to deposit. + * The user must approve this bridge contract to transfer the users NFT before this function + * is called. This function assumes the NFT contract complies with the ERC721 standard. + * For a transfer from another NFTVault, this method is called by the NFTVault that is sending the NFT. + * + * @param _virtualAssetId - the virutal asset id of the note returned in the deposit step of the convert function + * @param _collection - collection address of the NFT + * @param _tokenId - the token id of the NFT + */ + + function matchAndPull(uint256 _virtualAssetId, address _collection, uint256 _tokenId) external { + if (nftAssets[_virtualAssetId].collection != address(0x0)) { + revert InvalidVirtualAssetId(); + } + nftAssets[_virtualAssetId] = NFTAsset({collection: _collection, tokenId: _tokenId}); + IERC721(_collection).transferFrom(msg.sender, address(this), _tokenId); + emit NFTDeposit(_virtualAssetId, _collection, _tokenId); + } +} diff --git a/src/bridges/nft_trading/NFTVault.sol b/src/bridges/nft_trading/NFTVault.sol new file mode 100644 index 000000000..dec468119 --- /dev/null +++ b/src/bridges/nft_trading/NFTVault.sol @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec. +pragma solidity >=0.8.4; + +import {IERC721} from "../../../lib/openzeppelin-contracts/contracts/interfaces/IERC721.sol"; +import {AztecTypes} from "../../../lib/rollup-encoder/src/libraries/AztecTypes.sol"; +import {ErrorLib} from "../base/ErrorLib.sol"; +import {BridgeBase} from "../base/BridgeBase.sol"; +import {AddressRegistry} from "../registry/AddressRegistry.sol"; + +/** + * @title Basic NFT Vault for Aztec. + * @author Josh Crites, (@critesjosh on Github), Aztec Team + * @notice You can use this contract to hold your NFTs on Aztec. Whoever holds the corresponding virutal asset note can withdraw the NFT. + * @dev This bridge demonstrates basic functionality for an NFT bridge. This may be extended to support more features. + */ +contract NFTVault is BridgeBase { + struct NFTAsset { + address collection; + uint256 tokenId; + } + + AddressRegistry public immutable REGISTRY; + + mapping(uint256 => NFTAsset) public nftAssets; + + error InvalidVirtualAssetId(); + + event NFTDeposit(uint256 indexed virtualAssetId, address indexed collection, uint256 indexed tokenId); + event NFTWithdraw(uint256 indexed virtualAssetId, address indexed collection, uint256 indexed tokenId); + + /** + * @notice Set the addresses of RollupProcessor and AddressRegistry + * @param _rollupProcessor Address of the RollupProcessor + * @param _registry Address of the AddressRegistry + */ + constructor(address _rollupProcessor, address _registry) BridgeBase(_rollupProcessor) { + REGISTRY = AddressRegistry(_registry); + } + + /** + * @notice Function for the first step of a NFT deposit, a NFT withdrawal, or transfer to another NFTVault. + * @dev This method can only be called from the RollupProcessor. The first step of the + * deposit flow returns a virutal asset note that will represent the NFT on Aztec. After the + * virutal asset note is received on Aztec, the user calls matchAndPull which deposits the NFT + * into Aztec and matches it with the virtual asset. When the virutal asset is sent to this function + * it is burned and the NFT is sent to the recipient passed in _auxData. + * + * @param _inputAssetA - ETH (Deposit) or VIRTUAL (Withdrawal) + * @param _outputAssetA - VIRTUAL (Deposit) or 0 ETH (Withdrawal) + * @param _totalInputValue - must be 1 wei (Deposit) or 1 VIRTUAL (Withdrawal) + * @param _interactionNonce - A globally unique identifier of this interaction/`convert(...)` call + * corresponding to the returned virtual asset id + * @param _auxData - corresponds to the Ethereum address id in the AddressRegistry.sol for withdrawals + * @return outputValueA - 1 VIRTUAL asset (Deposit) or 0 ETH (Withdrawal) + * + */ + + function convert( + AztecTypes.AztecAsset calldata _inputAssetA, + AztecTypes.AztecAsset calldata, + AztecTypes.AztecAsset calldata _outputAssetA, + AztecTypes.AztecAsset calldata, + uint256 _totalInputValue, + uint256 _interactionNonce, + uint64 _auxData, + address + ) + external + payable + override (BridgeBase) + onlyRollup + returns (uint256 outputValueA, uint256 outputValueB, bool isAsync) + { + if ( + _inputAssetA.assetType == AztecTypes.AztecAssetType.NOT_USED + || _inputAssetA.assetType == AztecTypes.AztecAssetType.ERC20 + ) revert ErrorLib.InvalidInputA(); + if ( + _outputAssetA.assetType == AztecTypes.AztecAssetType.NOT_USED + || _outputAssetA.assetType == AztecTypes.AztecAssetType.ERC20 + ) revert ErrorLib.InvalidOutputA(); + if (_totalInputValue != 1) { + revert ErrorLib.InvalidInputAmount(); + } + if ( + _inputAssetA.assetType == AztecTypes.AztecAssetType.ETH + && _outputAssetA.assetType == AztecTypes.AztecAssetType.VIRTUAL + ) { + return (1, 0, false); + } else if (_inputAssetA.assetType == AztecTypes.AztecAssetType.VIRTUAL) { + NFTAsset memory token = nftAssets[_inputAssetA.id]; + if (token.collection == address(0x0)) { + revert ErrorLib.InvalidInputA(); + } + + address to = REGISTRY.addresses(_auxData); + if (to == address(0x0)) { + revert ErrorLib.InvalidAuxData(); + } + delete nftAssets[_inputAssetA.id]; + emit NFTWithdraw(_inputAssetA.id, token.collection, token.tokenId); + + if (_outputAssetA.assetType == AztecTypes.AztecAssetType.ETH) { + IERC721(token.collection).transferFrom(address(this), to, token.tokenId); + return (0, 0, false); + } else { + IERC721(token.collection).approve(to, token.tokenId); + NFTVault(to).matchAndPull(_interactionNonce, token.collection, token.tokenId); + return (1, 0, false); + } + } + } + + /** + * @notice Function for the second step of a NFT deposit or for transfers from other NFTVaults. + * @dev For a deposit, this method is called by an Ethereum L1 account that owns the NFT to deposit. + * The user must approve this bridge contract to transfer the users NFT before this function + * is called. This function assumes the NFT contract complies with the ERC721 standard. + * For a transfer from another NFTVault, this method is called by the NFTVault that is sending the NFT. + * + * @param _virtualAssetId - the virutal asset id of the note returned in the deposit step of the convert function + * @param _collection - collection address of the NFT + * @param _tokenId - the token id of the NFT + */ + + function matchAndPull(uint256 _virtualAssetId, address _collection, uint256 _tokenId) external { + if (nftAssets[_virtualAssetId].collection != address(0x0)) { + revert InvalidVirtualAssetId(); + } + nftAssets[_virtualAssetId] = NFTAsset({collection: _collection, tokenId: _tokenId}); + IERC721(_collection).transferFrom(msg.sender, address(this), _tokenId); + emit NFTDeposit(_virtualAssetId, _collection, _tokenId); + } +} diff --git a/src/bridges/registry/AddressRegistry.sol b/src/bridges/registry/AddressRegistry.sol new file mode 100644 index 000000000..cc3b68043 --- /dev/null +++ b/src/bridges/registry/AddressRegistry.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec. +pragma solidity >=0.8.4; + +import {AztecTypes} from "../../../lib/rollup-encoder/src/libraries/AztecTypes.sol"; +import {ErrorLib} from "../base/ErrorLib.sol"; +import {BridgeBase} from "../base/BridgeBase.sol"; + +/** + * @title Aztec Address Registry. + * @author Josh Crites (@critesjosh on Github), Aztec team + * @notice This contract can be used to anonymously register an ethereum address with an id. + * This is useful for reducing the amount of data required to pass an ethereum address through auxData. + * @dev Use this contract to lookup ethereum addresses by id. + */ +contract AddressRegistry is BridgeBase { + uint256 public addressCount; + mapping(uint256 => address) public addresses; + + event AddressRegistered(uint256 indexed index, address indexed entity); + + /** + * @notice Set address of rollup processor + * @param _rollupProcessor Address of rollup processor + */ + constructor(address _rollupProcessor) BridgeBase(_rollupProcessor) {} + + /** + * @notice Function for getting VIRTUAL assets (step 1) to register an address and registering an address (step 2). + * @dev This method can only be called from the RollupProcessor. The first step to register an address is for a user to + * get the type(uint160).max value of VIRTUAL assets back from the bridge. The second step is for the user + * to send an amount of VIRTUAL assets back to the bridge. The amount that is sent back is equal to the number of the + * ethereum address that is being registered (e.g. uint160(0x2e782B05290A7fFfA137a81a2bad2446AD0DdFEB)). + * + * @param _inputAssetA - ETH (step 1) or VIRTUAL (step 2) + * @param _outputAssetA - VIRTUAL (steps 1 and 2) + * @param _totalInputValue - must be 1 wei (ETH) (step 1) or address value (step 2) + * @return outputValueA - type(uint160).max (step 1) or 0 VIRTUAL (step 2) + * + */ + + function convert( + AztecTypes.AztecAsset calldata _inputAssetA, + AztecTypes.AztecAsset calldata, + AztecTypes.AztecAsset calldata _outputAssetA, + AztecTypes.AztecAsset calldata, + uint256 _totalInputValue, + uint256, + uint64, + address + ) external payable override (BridgeBase) onlyRollup returns (uint256 outputValueA, uint256, bool) { + if ( + _inputAssetA.assetType == AztecTypes.AztecAssetType.NOT_USED + || _inputAssetA.assetType == AztecTypes.AztecAssetType.ERC20 + ) revert ErrorLib.InvalidInputA(); + if (_outputAssetA.assetType != AztecTypes.AztecAssetType.VIRTUAL) { + revert ErrorLib.InvalidOutputA(); + } + if (_inputAssetA.assetType == AztecTypes.AztecAssetType.ETH) { + if (_totalInputValue != 1) { + revert ErrorLib.InvalidInputAmount(); + } + return (type(uint160).max, 0, false); + } else if (_inputAssetA.assetType == AztecTypes.AztecAssetType.VIRTUAL) { + address toRegister = address(uint160(_totalInputValue)); + registerAddress(toRegister); + return (0, 0, false); + } + } + + /** + * @notice Register an address at the registry + * @dev This function can be called directly from another Ethereum account. This can be done in + * one step, in one transaction. Coming from Ethereum directly, this method is not as privacy + * preserving as registering an address through the bridge. + * + * @param _to - The address to register + * @return addressCount - the index of address that has been registered + */ + + function registerAddress(address _to) public returns (uint256) { + uint256 userIndex = addressCount++; + addresses[userIndex] = _to; + emit AddressRegistered(userIndex, _to); + return userIndex; + } +} diff --git a/src/deployment/nft-basic/NFTVaultDeployment.s.sol b/src/deployment/nft-basic/NFTVaultDeployment.s.sol new file mode 100644 index 000000000..9d3e7dfe4 --- /dev/null +++ b/src/deployment/nft-basic/NFTVaultDeployment.s.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec. +pragma solidity >=0.8.4; + +import {BaseDeployment} from "../base/BaseDeployment.s.sol"; +import {NFTVault} from "../../bridges/nft-basic/NFTVault.sol"; +import {AddressRegistry} from "../../bridges/registry/AddressRegistry.sol"; + +contract NFTVaultDeployment is BaseDeployment { + function deploy(address _addressRegistry) public returns (address) { + emit log("Deploying NFTVault bridge"); + + vm.broadcast(); + NFTVault bridge = new NFTVault(ROLLUP_PROCESSOR, _addressRegistry); + + emit log_named_address("NFTVault bridge deployed to", address(bridge)); + + return address(bridge); + } + + function deployAndList(address _addressRegistry) public returns (address) { + address bridge = deploy(_addressRegistry); + + uint256 addressId = listBridge(bridge, 135500); + emit log_named_uint("NFTVault bridge address id", addressId); + + return bridge; + } + + function deployAndListAddressRegistry() public returns (address) { + emit log("Deploying AddressRegistry bridge"); + + AddressRegistry bridge = new AddressRegistry(ROLLUP_PROCESSOR); + + emit log_named_address("AddressRegistry bridge deployed to", address(bridge)); + + uint256 addressId = listBridge(address(bridge), 120500); + emit log_named_uint("AddressRegistry bridge address id", addressId); + + return address(bridge); + } +} diff --git a/src/deployment/registry/AddressRegistryDeployment.s.sol b/src/deployment/registry/AddressRegistryDeployment.s.sol new file mode 100644 index 000000000..52cd58b6b --- /dev/null +++ b/src/deployment/registry/AddressRegistryDeployment.s.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec. +pragma solidity >=0.8.4; + +import {BaseDeployment} from "../base/BaseDeployment.s.sol"; +import {AddressRegistry} from "../../bridges/registry/AddressRegistry.sol"; + +contract AddressRegistryDeployment is BaseDeployment { + function deploy() public returns (address) { + emit log("Deploying AddressRegistry bridge"); + + vm.broadcast(); + AddressRegistry bridge = new AddressRegistry(ROLLUP_PROCESSOR); + + emit log_named_address("AddressRegistry bridge deployed to", address(bridge)); + + return address(bridge); + } + + function deployAndList() public returns (address) { + address bridge = deploy(); + + uint256 addressId = listBridge(bridge, 120500); + emit log_named_uint("AddressRegistry bridge address id", addressId); + + return bridge; + } +} diff --git a/src/test/bridges/nft-basic/NFTVaultBasicE2E.t.sol b/src/test/bridges/nft-basic/NFTVaultBasicE2E.t.sol new file mode 100644 index 000000000..6858227e8 --- /dev/null +++ b/src/test/bridges/nft-basic/NFTVaultBasicE2E.t.sol @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec. +pragma solidity >=0.8.4; + +import {BridgeTestBase} from "./../../aztec/base/BridgeTestBase.sol"; +import {AztecTypes} from "rollup-encoder/libraries/AztecTypes.sol"; + +// Example-specific imports +import {NFTVault} from "../../../bridges/nft-basic/NFTVault.sol"; +import {AddressRegistry} from "../../../bridges/registry/AddressRegistry.sol"; +import {ErrorLib} from "../../../bridges/base/ErrorLib.sol"; +import {ERC721PresetMinterPauserAutoId} from + "@openzeppelin/contracts/token/ERC721/presets/ERC721PresetMinterPauserAutoId.sol"; + +/** + * @notice The purpose of this test is to test the bridge in an environment that is as close to the final deployment + * as possible without spinning up all the rollup infrastructure (sequencer, proof generator etc.). + */ +contract NFTVaultBasicE2ETest is BridgeTestBase { + NFTVault internal bridge; + NFTVault internal bridge2; + AddressRegistry private registry; + ERC721PresetMinterPauserAutoId private nftContract; + + // To store the id of the bridge after being added + uint256 private bridgeId; + uint256 private bridge2Id; + uint256 private registryBridgeId; + uint256 private tokenIdToDeposit = 1; + address private constant REGISTER_ADDRESS = 0x2e782B05290A7fFfA137a81a2bad2446AD0DdFEA; + address private constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; + AztecTypes.AztecAsset private ethAsset; + AztecTypes.AztecAsset private virtualAsset1 = + AztecTypes.AztecAsset({id: 1, erc20Address: address(0), assetType: AztecTypes.AztecAssetType.VIRTUAL}); + AztecTypes.AztecAsset private virtualAsset100 = + AztecTypes.AztecAsset({id: 100, erc20Address: address(0), assetType: AztecTypes.AztecAssetType.VIRTUAL}); + AztecTypes.AztecAsset private erc20InputAsset = + AztecTypes.AztecAsset({id: 1, erc20Address: DAI, assetType: AztecTypes.AztecAssetType.ERC20}); + + event NFTDeposit(uint256 indexed virtualAssetId, address indexed collection, uint256 indexed tokenId); + event NFTWithdraw(uint256 indexed virtualAssetId, address indexed collection, uint256 indexed tokenId); + + function setUp() public { + registry = new AddressRegistry(address(ROLLUP_PROCESSOR)); + bridge = new NFTVault(address(ROLLUP_PROCESSOR), address(registry)); + bridge2 = new NFTVault(address(ROLLUP_PROCESSOR), address(registry)); + nftContract = new ERC721PresetMinterPauserAutoId("test", "NFT", ""); + nftContract.mint(address(this)); + nftContract.mint(address(this)); + nftContract.mint(address(this)); + + nftContract.approve(address(bridge), 0); + nftContract.approve(address(bridge), 1); + nftContract.approve(address(bridge), 2); + + ethAsset = ROLLUP_ENCODER.getRealAztecAsset(address(0)); + + vm.label(address(registry), "AddressRegistry Bridge"); + vm.label(address(bridge), "NFTVault Bridge"); + + // Impersonate the multi-sig to add a new bridge + vm.startPrank(MULTI_SIG); + + // WARNING: If you set this value too low the interaction will fail for seemingly no reason! + // OTOH if you se it too high bridge users will pay too much + ROLLUP_PROCESSOR.setSupportedBridge(address(registry), 120500); + ROLLUP_PROCESSOR.setSupportedBridge(address(bridge), 135500); + ROLLUP_PROCESSOR.setSupportedBridge(address(bridge2), 135500); + + vm.stopPrank(); + + // Fetch the id of the bridges + registryBridgeId = ROLLUP_PROCESSOR.getSupportedBridgesLength() - 2; + bridgeId = ROLLUP_PROCESSOR.getSupportedBridgesLength() - 1; + bridge2Id = ROLLUP_PROCESSOR.getSupportedBridgesLength(); + // get virtual assets to register an address + ROLLUP_ENCODER.defiInteractionL2(registryBridgeId, ethAsset, emptyAsset, virtualAsset1, emptyAsset, 0, 1); + ROLLUP_ENCODER.processRollup(); + // get virtual assets to register 2nd NFTVault + ROLLUP_ENCODER.defiInteractionL2(registryBridgeId, ethAsset, emptyAsset, virtualAsset1, emptyAsset, 0, 1); + ROLLUP_ENCODER.processRollup(); + + // register an address + uint160 inputAmount = uint160(REGISTER_ADDRESS); + ROLLUP_ENCODER.defiInteractionL2( + registryBridgeId, virtualAsset1, emptyAsset, virtualAsset1, emptyAsset, 0, inputAmount + ); + ROLLUP_ENCODER.processRollup(); + + // register 2nd NFTVault in AddressRegistry + uint160 bridge2AddressAmount = uint160(address(bridge2)); + ROLLUP_ENCODER.defiInteractionL2( + registryBridgeId, virtualAsset1, emptyAsset, virtualAsset1, emptyAsset, 0, bridge2AddressAmount + ); + } + + function testDeposit() public { + // get virtual asset before deposit + ROLLUP_ENCODER.defiInteractionL2(bridgeId, ethAsset, emptyAsset, virtualAsset100, emptyAsset, 0, 1); + + (uint256 outputValueA, uint256 outputValueB, bool isAsync) = ROLLUP_ENCODER.processRollupAndGetBridgeResult(); + + assertEq(outputValueA, 1, "Output value A doesn't equal 1"); + assertEq(outputValueB, 0, "Output value B is not 0"); + assertTrue(!isAsync, "Bridge is incorrectly in an async mode"); + + address collection = address(nftContract); + + vm.expectEmit(true, true, true, false); + emit NFTDeposit(virtualAsset100.id, collection, tokenIdToDeposit); + bridge.matchAndPull(virtualAsset100.id, collection, tokenIdToDeposit); + (address returnedCollection, uint256 returnedId) = bridge.nftAssets(virtualAsset100.id); + assertEq(returnedId, tokenIdToDeposit, "nft token id does not match input"); + assertEq(returnedCollection, collection, "collection data does not match"); + } + + function testWithdraw() public { + testDeposit(); + uint64 auxData = uint64(registry.addressCount() - 2); + + vm.expectEmit(true, true, false, false); + emit NFTWithdraw(virtualAsset100.id, address(nftContract), tokenIdToDeposit); + ROLLUP_ENCODER.defiInteractionL2(bridgeId, virtualAsset100, emptyAsset, ethAsset, emptyAsset, auxData, 1); + + (uint256 outputValueA, uint256 outputValueB, bool isAsync) = ROLLUP_ENCODER.processRollupAndGetBridgeResult(); + address owner = nftContract.ownerOf(tokenIdToDeposit); + assertEq(REGISTER_ADDRESS, owner, "registered address is not the owner"); + assertEq(outputValueA, 0, "Output value A is not 0"); + assertEq(outputValueB, 0, "Output value B is not 0"); + assertTrue(!isAsync, "Bridge is incorrectly in an async mode"); + + (address _a, uint256 _id) = bridge.nftAssets(virtualAsset100.id); + assertEq(_a, address(0), "collection address is not 0"); + assertEq(_id, 0, "token id is not 0"); + } + + function testTransfer() public { + testDeposit(); + (address collection, uint256 tokenId) = bridge.nftAssets(virtualAsset100.id); + uint64 auxData = uint64(registry.addressCount() - 1); + + uint256 interactionNonce = ROLLUP_ENCODER.getNextNonce(); + + vm.expectEmit(true, true, true, false, address(bridge)); + emit NFTWithdraw(virtualAsset100.id, collection, tokenId); + vm.expectEmit(true, true, true, false, address(bridge2)); + emit NFTDeposit(interactionNonce, collection, tokenId); + ROLLUP_ENCODER.defiInteractionL2(bridgeId, virtualAsset100, emptyAsset, virtualAsset1, emptyAsset, auxData, 1); + + ROLLUP_ENCODER.processRollup(); + + // check that the nft was transferred to the second NFTVault + (address returnedCollection, uint256 returnedId) = bridge2.nftAssets(interactionNonce); + assertEq(returnedId, tokenIdToDeposit, "nft token id does not match input"); + assertEq(returnedCollection, collection, "collection data does not match"); + } +} diff --git a/src/test/bridges/nft-basic/NFTVaultBasicUnit.t.sol b/src/test/bridges/nft-basic/NFTVaultBasicUnit.t.sol new file mode 100644 index 000000000..7fdf5ce43 --- /dev/null +++ b/src/test/bridges/nft-basic/NFTVaultBasicUnit.t.sol @@ -0,0 +1,257 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec. +pragma solidity >=0.8.4; + +import {BridgeTestBase} from "./../../aztec/base/BridgeTestBase.sol"; +import {AztecTypes} from "rollup-encoder/libraries/AztecTypes.sol"; + +// Example-specific imports +import {ERC721PresetMinterPauserAutoId} from + "@openzeppelin/contracts/token/ERC721/presets/ERC721PresetMinterPauserAutoId.sol"; +import {NFTVault} from "../../../bridges/nft-basic/NFTVault.sol"; +import {ErrorLib} from "../../../bridges/base/ErrorLib.sol"; +import {AddressRegistry} from "../../../bridges/registry/AddressRegistry.sol"; + +// @notice The purpose of this test is to directly test convert functionality of the bridge. +contract NFTVaultBasicUnitTest is BridgeTestBase { + struct NftAsset { + address collection; + uint256 id; + } + + address private rollupProcessor; + + NFTVault private bridge; + NFTVault private bridge2; + ERC721PresetMinterPauserAutoId private nftContract; + uint256 private tokenIdToDeposit = 1; + AddressRegistry private registry; + address private constant REGISTER_ADDRESS = 0x2e782B05290A7fFfA137a81a2bad2446AD0DdFEA; + address private constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; + + AztecTypes.AztecAsset private ethAsset = + AztecTypes.AztecAsset({id: 0, erc20Address: address(0), assetType: AztecTypes.AztecAssetType.ETH}); + AztecTypes.AztecAsset private virtualAsset1 = + AztecTypes.AztecAsset({id: 1, erc20Address: address(0), assetType: AztecTypes.AztecAssetType.VIRTUAL}); + AztecTypes.AztecAsset private virtualAsset100 = + AztecTypes.AztecAsset({id: 100, erc20Address: address(0), assetType: AztecTypes.AztecAssetType.VIRTUAL}); + AztecTypes.AztecAsset private erc20InputAsset = + AztecTypes.AztecAsset({id: 1, erc20Address: DAI, assetType: AztecTypes.AztecAssetType.ERC20}); + + // @dev This method exists on RollupProcessor.sol. It's defined here in order to be able to receive ETH like a real + // rollup processor would. + function receiveEthFromBridge(uint256 _interactionNonce) external payable {} + + function setUp() public { + // In unit tests we set address of rollupProcessor to the address of this test contract + rollupProcessor = address(this); + + registry = new AddressRegistry(rollupProcessor); + bridge = new NFTVault(rollupProcessor, address(registry)); + bridge2 = new NFTVault(rollupProcessor, address(registry)); + nftContract = new ERC721PresetMinterPauserAutoId("test", "NFT", ""); + nftContract.mint(address(this)); + nftContract.mint(address(this)); + nftContract.mint(address(this)); + + nftContract.approve(address(bridge), 0); + nftContract.approve(address(bridge), 1); + nftContract.approve(address(bridge), 2); + + _registerAddress(REGISTER_ADDRESS); + _registerAddress(address(bridge2)); + + // Set ETH balance of bridge to 0 for clarity (somebody sent ETH to that address on mainnet) + vm.deal(address(bridge), 0); + vm.label(address(bridge), "Basic NFT Vault Bridge"); + } + + function testInvalidCaller(address _callerAddress) public { + vm.assume(_callerAddress != rollupProcessor); + // Use HEVM cheatcode to call from a different address than is address(this) + vm.prank(_callerAddress); + vm.expectRevert(ErrorLib.InvalidCaller.selector); + bridge.convert(emptyAsset, emptyAsset, emptyAsset, emptyAsset, 0, 0, 0, address(0)); + } + + function testInvalidInputAssetType() public { + vm.expectRevert(ErrorLib.InvalidInputA.selector); + bridge.convert(emptyAsset, emptyAsset, emptyAsset, emptyAsset, 0, 0, 0, address(0)); + } + + function testInvalidOutputAssetType() public { + vm.expectRevert(ErrorLib.InvalidOutputA.selector); + bridge.convert(ethAsset, emptyAsset, erc20InputAsset, emptyAsset, 0, 0, 0, address(0)); + } + + function testGetVirtualAssetUnitTest() public { + vm.warp(block.timestamp + 1 days); + + (uint256 outputValueA, uint256 outputValueB, bool isAsync) = bridge.convert( + ethAsset, // _inputAssetA + emptyAsset, // _inputAssetB + virtualAsset100, // _outputAssetA + emptyAsset, // _outputAssetB + 1, // _totalInputValue + 0, // _interactionNonce + 0, // _auxData + address(0) // _rollupBeneficiary + ); + + assertEq(outputValueA, 1, "Output value A doesn't equal 1"); + assertEq(outputValueB, 0, "Output value B is not 0"); + assertTrue(!isAsync, "Bridge is incorrectly in an async mode"); + } + + // should fail because sending more than 1 wei + function testGetVirtualAssetShouldFail() public { + vm.warp(block.timestamp + 1 days); + + vm.expectRevert(); + bridge.convert( + ethAsset, // _inputAssetA + emptyAsset, // _inputAssetB + virtualAsset100, // _outputAssetA + emptyAsset, // _outputAssetB + 2, // _totalInputValue + 0, // _interactionNonce + 0, // _auxData + address(0) // _rollupBeneficiary + ); + } + + function testDeposit() public { + vm.warp(block.timestamp + 1 days); + + address collection = address(nftContract); + bridge.matchAndPull(virtualAsset100.id, collection, tokenIdToDeposit); + (address returnedCollection, uint256 returnedId) = bridge.nftAssets(virtualAsset100.id); + assertEq(returnedId, tokenIdToDeposit, "nft token id does not match input"); + assertEq(returnedCollection, collection, "collection data does not match"); + } + + // should fail because an NFT with this id has already been deposited + function testDepositFailWithDuplicateNft() public { + testDeposit(); + vm.warp(block.timestamp + 1 days); + + address collection = address(nftContract); + vm.expectRevert(); + bridge.matchAndPull(virtualAsset100.id, collection, tokenIdToDeposit); + } + + // should fail because no withdraw address has been registered with this id + function testWithdrawUnregisteredWithdrawAddress() public { + testDeposit(); + uint64 auxData = 1000; + vm.expectRevert(ErrorLib.InvalidAuxData.selector); + bridge.convert( + virtualAsset100, // _inputAssetA + emptyAsset, // _inputAssetB + ethAsset, // _outputAssetA + emptyAsset, // _outputAssetB + 1, // _totalInputValue + 0, // _interactionNonce + auxData, + address(0) + ); + } + + function testWithdraw() public { + testDeposit(); + uint64 auxData = uint64(registry.addressCount() - 2); + (uint256 outputValueA, uint256 outputValueB, bool isAsync) = bridge.convert( + virtualAsset100, // _inputAssetA + emptyAsset, // _inputAssetB + ethAsset, // _outputAssetA + emptyAsset, // _outputAssetB + 1, // _totalInputValue + 0, // _interactionNonce + auxData, // _auxData + address(0) + ); + address owner = nftContract.ownerOf(tokenIdToDeposit); + assertEq(REGISTER_ADDRESS, owner, "registered address is not the owner"); + assertEq(outputValueA, 0, "Output value A is not 0"); + assertEq(outputValueB, 0, "Output value B is not 0"); + assertTrue(!isAsync, "Bridge is incorrectly in an async mode"); + + (address _a, uint256 _id) = bridge.nftAssets(virtualAsset100.id); + assertEq(_a, address(0), "collection address is not 0"); + assertEq(_id, 0, "token id is not 0"); + } + + // should fail because no NFT has been registered with this virtual asset + function testWithdrawUnregisteredNft() public { + testDeposit(); + uint64 auxData = uint64(registry.addressCount()); + vm.expectRevert(ErrorLib.InvalidInputA.selector); + bridge.convert( + virtualAsset1, // _inputAssetA + emptyAsset, // _inputAssetB + ethAsset, // _outputAssetA + emptyAsset, // _outputAssetB + 1, // _totalInputValue + 0, // _interactionNonce + auxData, + address(0) + ); + } + + function testTransfer() public { + testDeposit(); + uint64 auxData = uint64(registry.addressCount() - 1); + uint256 interactionNonce = 128; + + (uint256 outputValueA, uint256 outputValueB, bool isAsync) = bridge.convert( + virtualAsset100, // _inputAssetA + emptyAsset, // _inputAssetB + virtualAsset100, // _outputAssetA + emptyAsset, // _outputAssetB + 1, // _totalInputValue + interactionNonce, // _interactionNonce + auxData, // _auxData + address(0) + ); + address owner = nftContract.ownerOf(tokenIdToDeposit); + assertEq(address(bridge2), owner, "registered address is not the owner"); + assertEq(outputValueA, 1, "Output value A is not 1"); + assertEq(outputValueB, 0, "Output value B is not 0"); + assertTrue(!isAsync, "Bridge is incorrectly in an async mode"); + + // test that the nft was deleted from bridge 1 + (address bridge1collection,) = bridge.nftAssets(virtualAsset100.id); + assertEq(bridge1collection, address(0), "collection was not deleted"); + + // test that the nft was added to bridge 2 + (address _a, uint256 _id) = bridge2.nftAssets(interactionNonce); + assertEq(_a, address(nftContract), "collection address is not 0"); + assertEq(_id, tokenIdToDeposit, "token id is not 0"); + } + + function _registerAddress(address _addressToRegister) internal { + // get virtual assets + registry.convert( + ethAsset, + emptyAsset, + virtualAsset1, + emptyAsset, + 1, // _totalInputValue + 0, // _interactionNonce + 0, // _auxData + address(0x0) + ); + uint256 inputAmount = uint160(address(_addressToRegister)); + // register an address + registry.convert( + virtualAsset1, + emptyAsset, + virtualAsset1, + emptyAsset, + inputAmount, + 0, // _interactionNonce + 0, // _auxData + address(0x0) + ); + } +} diff --git a/src/test/bridges/registry/AddressRegistryE2E.t.sol b/src/test/bridges/registry/AddressRegistryE2E.t.sol new file mode 100644 index 000000000..2191ac198 --- /dev/null +++ b/src/test/bridges/registry/AddressRegistryE2E.t.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec. +pragma solidity >=0.8.4; + +import {BridgeTestBase} from "./../../aztec/base/BridgeTestBase.sol"; +import {AztecTypes} from "../../../../lib/rollup-encoder/src/libraries/AztecTypes.sol"; + +// Example-specific imports +import {AddressRegistry} from "../../../bridges/registry/AddressRegistry.sol"; +import {ErrorLib} from "../../../bridges/base/ErrorLib.sol"; + +/** + * @notice The purpose of this test is to test the bridge in an environment that is as close to the final deployment + * as possible without spinning up all the rollup infrastructure (sequencer, proof generator etc.). + */ +contract AddressRegistryE2ETest is BridgeTestBase { + AddressRegistry internal bridge; + uint256 private id; + AztecTypes.AztecAsset private ethAsset; + AztecTypes.AztecAsset private virtualAsset1; + uint256 public maxInt = type(uint160).max; + + event AddressRegistered(uint256 indexed addressCount, address indexed registeredAddress); + + function setUp() public { + bridge = new AddressRegistry(address(ROLLUP_PROCESSOR)); + ethAsset = ROLLUP_ENCODER.getRealAztecAsset(address(0)); + virtualAsset1 = + AztecTypes.AztecAsset({id: 0, erc20Address: address(0), assetType: AztecTypes.AztecAssetType.VIRTUAL}); + + vm.label(address(bridge), "Address Registry Bridge"); + + // Impersonate the multi-sig to add a new bridge + vm.startPrank(MULTI_SIG); + + // WARNING: If you set this value too low the interaction will fail for seemingly no reason! + // OTOH if you se it too high bridge users will pay too much + ROLLUP_PROCESSOR.setSupportedBridge(address(bridge), 120000); + + vm.stopPrank(); + + // Fetch the id of the example bridge + id = ROLLUP_PROCESSOR.getSupportedBridgesLength(); + } + + function testGetVirtualAssets() public { + vm.warp(block.timestamp + 1 days); + + ROLLUP_ENCODER.defiInteractionL2(id, ethAsset, emptyAsset, virtualAsset1, emptyAsset, 0, 1); + + (uint256 outputValueA, uint256 outputValueB, bool isAsync) = ROLLUP_ENCODER.processRollupAndGetBridgeResult(); + + assertEq(outputValueA, maxInt, "outputValueA doesn't equal maxInt"); + assertEq(outputValueB, 0, "Non-zero outputValueB"); + assertFalse(isAsync, "Bridge is not synchronous"); + } + + function testRegistration() public { + uint160 inputAmount = uint160(0x2e782B05290A7fFfA137a81a2bad2446AD0DdFEA); + + vm.expectEmit(true, true, false, false); + emit AddressRegistered(0, address(inputAmount)); + + ROLLUP_ENCODER.defiInteractionL2(id, virtualAsset1, emptyAsset, virtualAsset1, emptyAsset, 0, inputAmount); + + (uint256 outputValueA, uint256 outputValueB, bool isAsync) = ROLLUP_ENCODER.processRollupAndGetBridgeResult(); + + uint64 addressId = uint64(bridge.addressCount()) - 1; + address newlyRegistered = bridge.addresses(addressId); + + assertEq(address(inputAmount), newlyRegistered, "input amount doesn't equal newly registered address"); + assertEq(outputValueA, 0, "Non-zero outputValueA"); + assertEq(outputValueB, 0, "Non-zero outputValueB"); + assertFalse(isAsync, "Bridge is not synchronous"); + } +} diff --git a/src/test/bridges/registry/AddressRegistryUnitTest.t.sol b/src/test/bridges/registry/AddressRegistryUnitTest.t.sol new file mode 100644 index 000000000..a66fcd274 --- /dev/null +++ b/src/test/bridges/registry/AddressRegistryUnitTest.t.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec. +pragma solidity >=0.8.4; + +import {BridgeTestBase} from "./../../aztec/base/BridgeTestBase.sol"; +import {AztecTypes} from "rollup-encoder/libraries/AztecTypes.sol"; + +// Example-specific imports +import {AddressRegistry} from "../../../bridges/registry/AddressRegistry.sol"; +import {ErrorLib} from "../../../bridges/base/ErrorLib.sol"; + +// @notice The purpose of this test is to directly test convert functionality of the bridge. +contract AddressRegistryUnitTest is BridgeTestBase { + address private constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; + address private rollupProcessor; + AddressRegistry private bridge; + uint256 public maxInt = type(uint160).max; + AztecTypes.AztecAsset private ethAsset = + AztecTypes.AztecAsset({id: 0, erc20Address: address(0), assetType: AztecTypes.AztecAssetType.ETH}); + AztecTypes.AztecAsset private virtualAsset = + AztecTypes.AztecAsset({id: 0, erc20Address: address(0), assetType: AztecTypes.AztecAssetType.VIRTUAL}); + AztecTypes.AztecAsset private daiAsset = + AztecTypes.AztecAsset({id: 1, erc20Address: DAI, assetType: AztecTypes.AztecAssetType.ERC20}); + + event AddressRegistered(uint256 indexed addressCount, address indexed registeredAddress); + + // @dev This method exists on RollupProcessor.sol. It's defined here in order to be able to receive ETH like a real + // rollup processor would. + function receiveEthFromBridge(uint256 _interactionNonce) external payable {} + + function setUp() public { + // In unit tests we set address of rollupProcessor to the address of this test contract + rollupProcessor = address(this); + + bridge = new AddressRegistry(rollupProcessor); + + // Use the label cheatcode to mark the address with "AddressRegistry Bridge" in the traces + vm.label(address(bridge), "AddressRegistry Bridge"); + } + + function testInvalidCaller(address _callerAddress) public { + vm.assume(_callerAddress != rollupProcessor); + // Use HEVM cheatcode to call from a different address than is address(this) + vm.prank(_callerAddress); + vm.expectRevert(ErrorLib.InvalidCaller.selector); + bridge.convert(emptyAsset, emptyAsset, emptyAsset, emptyAsset, 0, 0, 0, address(0)); + } + + function testInvalidInputAssetType() public { + vm.expectRevert(ErrorLib.InvalidInputA.selector); + bridge.convert(daiAsset, emptyAsset, emptyAsset, emptyAsset, 0, 0, 0, address(0)); + } + + function testInvalidOutputAssetType() public { + vm.expectRevert(ErrorLib.InvalidOutputA.selector); + bridge.convert(ethAsset, emptyAsset, daiAsset, emptyAsset, 0, 0, 0, address(0)); + } + + function testInvalidInputAmount() public { + vm.expectRevert(ErrorLib.InvalidInputAmount.selector); + + bridge.convert( + ethAsset, + emptyAsset, + virtualAsset, + emptyAsset, + 0, // _totalInputValue + 0, // _interactionNonce + 0, // _auxData + address(0x0) + ); + } + + function testGetBackMaxVirtualAssets() public { + vm.warp(block.timestamp + 1 days); + + (uint256 outputValueA, uint256 outputValueB, bool isAsync) = bridge.convert( + ethAsset, + emptyAsset, + virtualAsset, + emptyAsset, + 1, // _totalInputValue + 0, // _interactionNonce + 0, // _auxData + address(0x0) + ); + + assertEq(outputValueA, maxInt, "Output value A doesn't equal maxInt"); + assertEq(outputValueB, 0, "Output value B is not 0"); + assertTrue(!isAsync, "Bridge is incorrectly in an async mode"); + } + + function testRegistringAnAddress() public { + vm.warp(block.timestamp + 1 days); + + uint160 inputAmount = uint160(0x2e782B05290A7fFfA137a81a2bad2446AD0DdFEA); + + vm.expectEmit(true, true, false, false); + emit AddressRegistered(0, address(inputAmount)); + + (uint256 outputValueA, uint256 outputValueB, bool isAsync) = bridge.convert( + virtualAsset, + emptyAsset, + virtualAsset, + emptyAsset, + inputAmount, // _totalInputValue + 0, // _interactionNonce + 0, // _auxData + address(0x0) + ); + + uint256 id = bridge.addressCount() - 1; + address newlyRegistered = bridge.addresses(id); + + assertEq(address(inputAmount), newlyRegistered, "Address not registered"); + assertEq(outputValueA, 0, "Output value is not 0"); + assertEq(outputValueB, 0, "Output value B is not 0"); + assertTrue(!isAsync, "Bridge is incorrectly in an async mode"); + } + + function testRegisterFromEth() public { + address to = address(0x2e782B05290A7fFfA137a81a2bad2446AD0DdFEA); + uint256 count = bridge.registerAddress(to); + address registered = bridge.addresses(count); + assertEq(to, registered, "Address not registered"); + } +} From 8244828f3aad97338a0029d6a50587f2ad1d4dcd Mon Sep 17 00:00:00 2001 From: SanChuan <2194167956@qq.com> Date: Thu, 5 Jan 2023 12:26:02 +0000 Subject: [PATCH 06/24] add specs --- specs/bridges/nft_trading/readme.md | 46 +++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 specs/bridges/nft_trading/readme.md diff --git a/specs/bridges/nft_trading/readme.md b/specs/bridges/nft_trading/readme.md new file mode 100644 index 000000000..1e226df85 --- /dev/null +++ b/specs/bridges/nft_trading/readme.md @@ -0,0 +1,46 @@ +# Spec for NFT Trading Bridge + +## What does the bridge do? Why build it? + +This bridge enables user to trading their NFT on the established Layer 1 NFT marketplaces, aka [OpenSea](https://opensea.io/), where users can list and purchase NFTs from Aztec L2 without revealing their L1 identities. +In this way, the owner of one NFT owner is in private with the power of Aztec's zero knowledge rollup. So this bridge can enhance the secret characterist of Layer 1 user. + + +## What protocol(s) does the bridge interact with ? + + +The bridge interacts with [OpenSea](https://opensea.io/). + +## What is the flow of the bridge? +The simple flow as below: +1. User A on Aztec chain bridge interface pay and buy an NFT on Opensea in Ethereum. +2. The "Buy" order and relevant fee is send to L1 by Aztec's rollup. +3. The bridge contract in L1 is triggered to interact with OpenSea protocol. +4. Then the bridge contract own this NFT and record it to a private user id. +5. The User A On Aztec can redeem this NFT anytime, with his valid signature. + + + +### General Properties of convert(...) function + +- The bridge is synchronous, and will always return `isAsync = false`. + +- The bridge uses `_auxData` to encode the target NFT, id, price. + +- The Bridge perform token pre-approvals to allow the `ROLLUP_PROCESSOR` and `UNI_ROUTER` to pull tokens from it. + This is to reduce gas-overhead when performing the actions. It is safe to do, as the bridge is not holding the funds itself. + +## Is the contract upgradeable? + +No, the bridge is immutable without any admin role. + +## Does the bridge maintain state? + +No, the bridge doesn't maintain a state. +However, it keeps an insignificant amount of tokens (dust) in the bridge to reduce gas-costs of future transactions (in case the DUST was sent to the bridge). +By having a dust, we don't need to do a `sstore` from `0` to `non-zero`. + +## Does this bridge maintain state? If so, what is stored and why? + +Yes, this bridge maintain the NFT bought from NFT to relevant user's private identity. +This state is a record for user to redeem their NFT asset. \ No newline at end of file From 34a70beac71a6937bd264a9c7870548851c2a3d8 Mon Sep 17 00:00:00 2001 From: SanChuan <2194167956@qq.com> Date: Thu, 5 Jan 2023 12:34:24 +0000 Subject: [PATCH 07/24] copy templete --- src/bridges/nft_trading/NftTradingBridge.sol | 584 +++++++++++++++++++ 1 file changed, 584 insertions(+) create mode 100644 src/bridges/nft_trading/NftTradingBridge.sol diff --git a/src/bridges/nft_trading/NftTradingBridge.sol b/src/bridges/nft_trading/NftTradingBridge.sol new file mode 100644 index 000000000..a87725082 --- /dev/null +++ b/src/bridges/nft_trading/NftTradingBridge.sol @@ -0,0 +1,584 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec +pragma solidity >=0.8.4; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {AztecTypes} from "rollup-encoder/libraries/AztecTypes.sol"; +import {IRollupProcessor} from "rollup-encoder/interfaces/IRollupProcessor.sol"; +import {ErrorLib} from "../base/ErrorLib.sol"; +import {BridgeBase} from "../base/BridgeBase.sol"; +import {ISwapRouter} from "../../interfaces/uniswapv3/ISwapRouter.sol"; +import {IWETH} from "../../interfaces/IWETH.sol"; +import {IQuoter} from "../../interfaces/uniswapv3/IQuoter.sol"; + +/** + * @title Aztec Connect Bridge for Trading on Opensea NFT market + * @author Sanchuan + * @notice You can use this contract to swap tokens on Uniswap v3 along complex paths. + * @dev Encoding of a path allows for up to 2 split paths (see the definition bellow) and up to 3 pools (2 middle + * tokens) in each split path. A path is encoded in _auxData parameter passed to the convert method. _auxData + * carry 64 bits of information. Along with split paths there is a minimum price encoded in auxData. + * + * Each split path takes 19 bits. Minimum price is encoded in 26 bits. Values are placed in the data as follows: + * |26 bits minimum price| |19 bits split path 2| |19 bits split path 1| + * + * Encoding of a split path is: + * |7 bits percentage| |2 bits fee| |3 bits middle token| |2 bits fee| |3 bits middle token| |2 bits fee| + * The meaning of percentage is how much of input amount will be routed through the corresponding split path. + * Fee bits are mapped to specific fee tiers as follows: + * 00 is 0.01%, 01 is 0.05%, 10 is 0.3%, 11 is 1% + * Middle tokens use the following mapping: + * 001 is ETH, 010 is USDC, 011 is USDT, 100 is DAI, 101 is WBTC, 110 is FRAX, 111 is BUSD. + * 000 means the middle token is unused. + * + * Min price is encoded as a floating point number. First 21 bits are used for significand, last 5 bits for + * exponent: |21 bits significand| |5 bits exponent| + * Minimum amount out is computed with the following formula: + * (inputValue * (significand * 10**exponent)) / (10 ** inputAssetDecimals) + * Here are 2 examples. + * 1) If I want to receive 10k Dai for 1 ETH I would set significand to 1 and exponent to 22. + * _totalInputValue = 1e18, asset = ETH (18 decimals), outputAssetA: Dai (18 decimals) + * (1e18 * (1 * 10**22)) / (10**18) = 1e22 --> 10k Dai + * 2) If I want to receive 2000 USDC for 1 ETH, I set significand to 2 and exponent to 9. + * _totalInputValue = 1e18, asset = ETH (18 decimals), outputAssetA: USDC (6 decimals) + * (1e18 * (2 * 10**9)) / (10**18) = 2e9 --> 2000 USDC + * + * Definition of split path: Split path is a term we use when there are multiple (in this case 2) paths between + * which the input amount of tokens is split. As an example we can consider swapping 100 ETH to DAI. In this case + * there could be 2 split paths. 1st split path going through ETH-USDC 500 bps fee pool and USDC-DAI 100 bps fee + * pool and 2nd split path going directly to DAI using the ETH-DAI 500 bps pool. First split path could for + * example consume 80% of input (80 ETH) and the second split path the remaining 20% (20 ETH). + */ +contract UniswapBridge is BridgeBase { + using SafeERC20 for IERC20; + + error InvalidFeeTierEncoding(); + error InvalidFeeTier(); + error InvalidTokenEncoding(); + error InvalidToken(); + error InvalidPercentageAmounts(); + error InsufficientAmountOut(); + error Overflow(); + + // @notice A struct representing a path with 2 split paths. + struct Path { + uint256 percentage1; // Percentage of input to swap through splitPath1 + bytes splitPath1; // A path encoded in a format used by Uniswap's v3 router + uint256 percentage2; // Percentage of input to swap through splitPath2 + bytes splitPath2; // A path encoded in a format used by Uniswap's v3 router + uint256 minPrice; // Minimum acceptable price + } + + struct SplitPath { + uint256 percentage; // Percentage of swap amount to send through this split path + uint256 fee1; // 1st pool fee + address token1; // Address of the 1st pool's output token + uint256 fee2; // 2nd pool fee + address token2; // Address of the 2nd pool's output token + uint256 fee3; // 3rd pool fee + } + + // @dev Event which is emitted when the output token doesn't implement decimals(). + event DefaultDecimalsWarning(); + + ISwapRouter public constant ROUTER = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); + IQuoter public constant QUOTER = IQuoter(0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6); + + // Addresses of middle tokens + address public constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + address public constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + address public constant USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; + address public constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; + address public constant WBTC = 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599; + address public constant FRAX = 0x853d955aCEf822Db058eb8505911ED77F175b99e; + address public constant BUSD = 0x4Fabb145d64652a948d72533023f6E7A623C7C53; + + uint64 public constant SPLIT_PATH_BIT_LENGTH = 19; + uint64 public constant SPLIT_PATHS_BIT_LENGTH = 38; // SPLIT_PATH_BIT_LENGTH * 2 + uint64 public constant PRICE_BIT_LENGTH = 26; // 64 - SPLIT_PATHS_BIT_LENGTH + + // @dev The following masks are used to decode 2 split paths and minimum acceptable price from 1 uint64. + // Binary number 0000000000000000000000000000000000000000000001111111111111111111 (last 19 bits) + uint64 public constant SPLIT_PATH_MASK = 0x7FFFF; + + // Binary number 0000000000000000000000000000000000000011111111111111111111111111 (last 26 bits) + uint64 public constant PRICE_MASK = 0x3FFFFFF; + + // Binary number 0000000000000000000000000000000000000000000000000000000000011111 (last 5 bits) + uint64 public constant EXPONENT_MASK = 0x1F; + + // Binary number 11 + uint64 public constant FEE_MASK = 0x3; + // Binary number 111 + uint64 public constant TOKEN_MASK = 0x7; + + /** + * @notice Set the address of rollup processor. + * @param _rollupProcessor Address of rollup processor + */ + constructor(address _rollupProcessor) BridgeBase(_rollupProcessor) {} + + // @dev Empty method which is present here in order to be able to receive ETH when unwrapping WETH. + receive() external payable {} + + /** + * @notice Sets all the important approvals. + * @param _tokensIn - An array of address of input tokens (tokens to later swap in the convert(...) function) + * @param _tokensOut - An array of address of output tokens (tokens to later return to rollup processor) + * @dev SwapBridge never holds any ERC20 tokens after or before an invocation of any of its functions. For this + * reason the following is not a security risk and makes convert(...) function more gas efficient. + */ + function preApproveTokens(address[] calldata _tokensIn, address[] calldata _tokensOut) external { + uint256 tokensLength = _tokensIn.length; + for (uint256 i; i < tokensLength;) { + address tokenIn = _tokensIn[i]; + // Using safeApprove(...) instead of approve(...) and first setting the allowance to 0 because underlying + // can be Tether + IERC20(tokenIn).safeApprove(address(ROUTER), 0); + IERC20(tokenIn).safeApprove(address(ROUTER), type(uint256).max); + unchecked { + ++i; + } + } + tokensLength = _tokensOut.length; + for (uint256 i; i < tokensLength;) { + address tokenOut = _tokensOut[i]; + // Using safeApprove(...) instead of approve(...) and first setting the allowance to 0 because underlying + // can be Tether + IERC20(tokenOut).safeApprove(address(ROLLUP_PROCESSOR), 0); + IERC20(tokenOut).safeApprove(address(ROLLUP_PROCESSOR), type(uint256).max); + unchecked { + ++i; + } + } + } + + /** + * @notice Registers subsidy criteria for a given token pair. + * @param _tokenIn - Input token to swap + * @param _tokenOut - Output token to swap + */ + function registerSubsidyCriteria(address _tokenIn, address _tokenOut) external { + SUBSIDY.setGasUsageAndMinGasPerMinute({ + _criteria: _computeCriteria(_tokenIn, _tokenOut), + _gasUsage: uint32(300000), // 300k gas (Note: this is a gas usage when only 1 split path is used) + _minGasPerMinute: uint32(100) // 1 fully subsidized call per 2 days (300k / (24 * 60) / 2) + }); + } + + /** + * @notice A function which swaps input token for output token along the path encoded in _auxData. + * @param _inputAssetA - Input ERC20 token + * @param _outputAssetA - Output ERC20 token + * @param _totalInputValue - Amount of input token to swap + * @param _interactionNonce - Interaction nonce + * @param _auxData - Encoded path (gets decoded to Path struct) + * @param _rollupBeneficiary - Address which receives subsidy if the call is eligible for it + * @return outputValueA - The amount of output token received + */ + function convert( + AztecTypes.AztecAsset calldata _inputAssetA, + AztecTypes.AztecAsset calldata, + AztecTypes.AztecAsset calldata _outputAssetA, + AztecTypes.AztecAsset calldata, + uint256 _totalInputValue, + uint256 _interactionNonce, + uint64 _auxData, + address _rollupBeneficiary + ) external payable override (BridgeBase) onlyRollup returns (uint256 outputValueA, uint256, bool) { + // Accumulate subsidy to _rollupBeneficiary + SUBSIDY.claimSubsidy( + _computeCriteria(_inputAssetA.erc20Address, _outputAssetA.erc20Address), _rollupBeneficiary + ); + + bool inputIsEth = _inputAssetA.assetType == AztecTypes.AztecAssetType.ETH; + bool outputIsEth = _outputAssetA.assetType == AztecTypes.AztecAssetType.ETH; + + if (_inputAssetA.assetType != AztecTypes.AztecAssetType.ERC20 && !inputIsEth) { + revert ErrorLib.InvalidInputA(); + } + if (_outputAssetA.assetType != AztecTypes.AztecAssetType.ERC20 && !outputIsEth) { + revert ErrorLib.InvalidOutputA(); + } + + Path memory path = _decodePath( + inputIsEth ? WETH : _inputAssetA.erc20Address, _auxData, outputIsEth ? WETH : _outputAssetA.erc20Address + ); + + uint256 inputValueSplitPath1 = (_totalInputValue * path.percentage1) / 100; + + if (path.percentage1 != 0) { + // Swap using the first swap path + outputValueA = ROUTER.exactInput{value: inputIsEth ? inputValueSplitPath1 : 0}( + ISwapRouter.ExactInputParams({ + path: path.splitPath1, + recipient: address(this), + deadline: block.timestamp, + amountIn: inputValueSplitPath1, + amountOutMinimum: 0 + }) + ); + } + + if (path.percentage2 != 0) { + // Swap using the second swap path + uint256 inputValueSplitPath2 = _totalInputValue - inputValueSplitPath1; + outputValueA += ROUTER.exactInput{value: inputIsEth ? inputValueSplitPath2 : 0}( + ISwapRouter.ExactInputParams({ + path: path.splitPath2, + recipient: address(this), + deadline: block.timestamp, + amountIn: inputValueSplitPath2, + amountOutMinimum: 0 + }) + ); + } + + uint256 tokenInDecimals = 18; + if (!inputIsEth) { + try IERC20Metadata(_inputAssetA.erc20Address).decimals() returns (uint8 decimals) { + tokenInDecimals = decimals; + } catch (bytes memory) { + emit DefaultDecimalsWarning(); + } + } + uint256 amountOutMinimum = (_totalInputValue * path.minPrice) / 10 ** tokenInDecimals; + if (outputValueA < amountOutMinimum) revert InsufficientAmountOut(); + + if (outputIsEth) { + IWETH(WETH).withdraw(outputValueA); + IRollupProcessor(ROLLUP_PROCESSOR).receiveEthFromBridge{value: outputValueA}(_interactionNonce); + } + } + + /** + * @notice A function which encodes path to a format expected in _auxData of this.convert(...) + * @param _amountIn - Amount of tokenIn to swap + * @param _minAmountOut - Amount of tokenOut to receive + * @param _tokenIn - Address of _tokenIn (@dev used only to fetch decimals) + * @param _splitPath1 - Split path to encode + * @param _splitPath2 - Split path to encode + * @return Path encoded in a format expected in _auxData of this.convert(...) + * @dev This function is not optimized and is expected to be used on frontend and in tests. + * @dev Reverts when min price is bigger than max encodeable value. + */ + function encodePath( + uint256 _amountIn, + uint256 _minAmountOut, + address _tokenIn, + SplitPath calldata _splitPath1, + SplitPath calldata _splitPath2 + ) external view returns (uint64) { + if (_splitPath1.percentage + _splitPath2.percentage != 100) revert InvalidPercentageAmounts(); + + return uint64( + ( + _computeEncodedMinPrice(_amountIn, _minAmountOut, IERC20Metadata(_tokenIn).decimals()) + << SPLIT_PATHS_BIT_LENGTH + ) + (_encodeSplitPath(_splitPath1) << SPLIT_PATH_BIT_LENGTH) + _encodeSplitPath(_splitPath2) + ); + } + + /** + * @notice A function which encodes path to a format expected in _auxData of this.convert(...) + * @param _amountIn - Amount of tokenIn to swap + * @param _tokenIn - Address of _tokenIn (@dev used only to fetch decimals) + * @param _path - Split path to encode + * @param _tokenOut - Address of _tokenIn (@dev used only to fetch decimals) + * @return amountOut - + */ + function quote(uint256 _amountIn, address _tokenIn, uint64 _path, address _tokenOut) + external + returns (uint256 amountOut) + { + Path memory path = _decodePath(_tokenIn, _path, _tokenOut); + uint256 inputValueSplitPath1 = (_amountIn * path.percentage1) / 100; + + if (path.percentage1 != 0) { + // Swap using the first swap path + amountOut += QUOTER.quoteExactInput(path.splitPath1, inputValueSplitPath1); + } + + if (path.percentage2 != 0) { + // Swap using the second swap path + amountOut += QUOTER.quoteExactInput(path.splitPath2, _amountIn - inputValueSplitPath1); + } + } + + /** + * @notice Computes the criteria that is passed when claiming subsidy. + * @param _inputAssetA The input asset + * @param _outputAssetA The output asset + * @return The criteria + */ + function computeCriteria( + AztecTypes.AztecAsset calldata _inputAssetA, + AztecTypes.AztecAsset calldata, + AztecTypes.AztecAsset calldata _outputAssetA, + AztecTypes.AztecAsset calldata, + uint64 + ) public pure override (BridgeBase) returns (uint256) { + return _computeCriteria(_inputAssetA.erc20Address, _outputAssetA.erc20Address); + } + + function _computeCriteria(address _inputToken, address _outputToken) internal pure returns (uint256) { + return uint256(keccak256(abi.encodePacked(_inputToken, _outputToken))); + } + + /** + * @notice A function which computes min price and encodes it in the format used in this bridge. + * @param _amountIn - Amount of tokenIn to swap + * @param _minAmountOut - Amount of tokenOut to receive + * @param _tokenInDecimals - Number of decimals of tokenIn + * @return encodedMinPrice - Min acceptable encoded in a format used in this bridge. + * @dev This function is not optimized and is expected to be used on frontend and in tests. + * @dev Reverts when min price is bigger than max encodeable value. + */ + function _computeEncodedMinPrice(uint256 _amountIn, uint256 _minAmountOut, uint256 _tokenInDecimals) + internal + pure + returns (uint256 encodedMinPrice) + { + uint256 minPrice = (_minAmountOut * 10 ** _tokenInDecimals) / _amountIn; + // 2097151 = 2**21 - 1 --> this number and its multiples of 10 can be encoded without precision loss + if (minPrice <= 2097151) { + // minPrice is smaller than the boundary of significand --> significand = _x, exponent = 0 + encodedMinPrice = minPrice << 5; + } else { + uint256 exponent = 0; + while (minPrice > 2097151) { + minPrice /= 10; + ++exponent; + // 31 = 2**5 - 1 --> max exponent + if (exponent > 31) revert Overflow(); + } + encodedMinPrice = (minPrice << 5) + exponent; + } + } + + /** + * @notice A function which encodes a split path. + * @param _path - Split path to encode + * @return Encoded split path (in the last 19 bits of uint256) + * @dev In place of unused middle tokens leave address(0). + * @dev Fee tier corresponding to unused middle token is ignored. + */ + function _encodeSplitPath(SplitPath calldata _path) internal pure returns (uint256) { + if (_path.percentage == 0) return 0; + return (_path.percentage << 12) + (_encodeFeeTier(_path.fee1) << 10) + (_encodeMiddleToken(_path.token1) << 7) + + (_encodeFeeTier(_path.fee2) << 5) + (_encodeMiddleToken(_path.token2) << 2) + (_encodeFeeTier(_path.fee3)); + } + + /** + * @notice A function which encodes fee tier. + * @param _feeTier - Fee tier in bps + * @return Encoded fee tier (in the last 2 bits of uint256) + */ + function _encodeFeeTier(uint256 _feeTier) internal pure returns (uint256) { + if (_feeTier == 100) { + // Binary number 00 + return 0; + } + if (_feeTier == 500) { + // Binary number 01 + return 1; + } + if (_feeTier == 3000) { + // Binary number 10 + return 2; + } + if (_feeTier == 10000) { + // Binary number 11 + return 3; + } + revert InvalidFeeTier(); + } + + /** + * @notice A function which returns token encoding for a given token address. + * @param _token - Token address + * @return encodedToken - Encoded token (in the last 3 bits of uint256) + */ + function _encodeMiddleToken(address _token) internal pure returns (uint256 encodedToken) { + if (_token == address(0)) { + // unused token + return 0; + } + if (_token == WETH) { + // binary number 001 + return 1; + } + if (_token == USDC) { + // binary number 010 + return 2; + } + if (_token == USDT) { + // binary number 011 + return 3; + } + if (_token == DAI) { + // binary number 100 + return 4; + } + if (_token == WBTC) { + // binary number 101 + return 5; + } + if (_token == FRAX) { + // binary number 110 + return 6; + } + if (_token == BUSD) { + // binary number 111 + return 7; + } + revert InvalidToken(); + } + + /** + * @notice A function which deserializes encoded path to Path struct. + * @param _tokenIn - Input ERC20 token + * @param _encodedPath - Encoded path + * @param _tokenOut - Output ERC20 token + * @return path - Decoded/deserialized path struct + */ + function _decodePath(address _tokenIn, uint256 _encodedPath, address _tokenOut) + internal + pure + returns (Path memory path) + { + (uint256 percentage1, bytes memory splitPath1) = + _decodeSplitPath(_tokenIn, _encodedPath & SPLIT_PATH_MASK, _tokenOut); + path.percentage1 = percentage1; + path.splitPath1 = splitPath1; + + (uint256 percentage2, bytes memory splitPath2) = + _decodeSplitPath(_tokenIn, (_encodedPath >> SPLIT_PATH_BIT_LENGTH) & SPLIT_PATH_MASK, _tokenOut); + + if (percentage1 + percentage2 != 100) revert InvalidPercentageAmounts(); + + path.percentage2 = percentage2; + path.splitPath2 = splitPath2; + path.minPrice = _decodeMinPrice(_encodedPath >> SPLIT_PATHS_BIT_LENGTH); + } + + /** + * @notice A function which returns a percentage of input going through the split path and the split path encoded + * in a format compatible with Uniswap router. + * @param _tokenIn - Input ERC20 token + * @param _encodedSplitPath - Encoded split path (in the last 19 bits of uint256) + * @param _tokenOut - Output ERC20 token + * @return percentage - A percentage of input going through the corresponding split path + * @return splitPath - A split path encoded in a format compatible with Uniswap router + */ + function _decodeSplitPath(address _tokenIn, uint256 _encodedSplitPath, address _tokenOut) + internal + pure + returns (uint256 percentage, bytes memory splitPath) + { + uint256 fee3 = _encodedSplitPath & FEE_MASK; + uint256 middleToken2 = (_encodedSplitPath >> 2) & TOKEN_MASK; + uint256 fee2 = (_encodedSplitPath >> 5) & FEE_MASK; + uint256 middleToken1 = (_encodedSplitPath >> 7) & TOKEN_MASK; + uint256 fee1 = (_encodedSplitPath >> 10) & FEE_MASK; + percentage = _encodedSplitPath >> 12; + + if (middleToken1 != 0 && middleToken2 != 0) { + splitPath = abi.encodePacked( + _tokenIn, + _decodeFeeTier(fee1), + _decodeMiddleToken(middleToken1), + _decodeFeeTier(fee2), + _decodeMiddleToken(middleToken2), + _decodeFeeTier(fee3), + _tokenOut + ); + } else if (middleToken1 != 0) { + splitPath = abi.encodePacked( + _tokenIn, _decodeFeeTier(fee1), _decodeMiddleToken(middleToken1), _decodeFeeTier(fee3), _tokenOut + ); + } else if (middleToken2 != 0) { + splitPath = abi.encodePacked( + _tokenIn, _decodeFeeTier(fee2), _decodeMiddleToken(middleToken2), _decodeFeeTier(fee3), _tokenOut + ); + } else { + splitPath = abi.encodePacked(_tokenIn, _decodeFeeTier(fee3), _tokenOut); + } + } + + /** + * @notice A function which converts minimum price in a floating point format to integer. + * @param _encodedMinPrice - Encoded minimum price (in the last 26 bits of uint256) + * @return minPrice - Minimum acceptable price represented as an integer + */ + function _decodeMinPrice(uint256 _encodedMinPrice) internal pure returns (uint256 minPrice) { + // 21 bits significand, 5 bits exponent + uint256 significand = _encodedMinPrice >> 5; + uint256 exponent = _encodedMinPrice & EXPONENT_MASK; + minPrice = significand * 10 ** exponent; + } + + /** + * @notice A function which converts encoded fee tier to a fee tier in an integer format. + * @param _encodedFeeTier - Encoded fee tier (in the last 2 bits of uint256) + * @return feeTier - Decoded fee tier in an integer format + */ + function _decodeFeeTier(uint256 _encodedFeeTier) internal pure returns (uint24 feeTier) { + if (_encodedFeeTier == 0) { + // Binary number 00 + return uint24(100); + } + if (_encodedFeeTier == 1) { + // Binary number 01 + return uint24(500); + } + if (_encodedFeeTier == 2) { + // Binary number 10 + return uint24(3000); + } + if (_encodedFeeTier == 3) { + // Binary number 11 + return uint24(10000); + } + revert InvalidFeeTierEncoding(); + } + + /** + * @notice A function which returns token address for an encoded token. + * @param _encodedToken - Encoded token (in the last 3 bits of uint256) + * @return token - Token address + */ + function _decodeMiddleToken(uint256 _encodedToken) internal pure returns (address token) { + if (_encodedToken == 1) { + // binary number 001 + return WETH; + } + if (_encodedToken == 2) { + // binary number 010 + return USDC; + } + if (_encodedToken == 3) { + // binary number 011 + return USDT; + } + if (_encodedToken == 4) { + // binary number 100 + return DAI; + } + if (_encodedToken == 5) { + // binary number 101 + return WBTC; + } + if (_encodedToken == 6) { + // binary number 110 + return FRAX; + } + if (_encodedToken == 7) { + // binary number 111 + return BUSD; + } + revert InvalidTokenEncoding(); + } +} From e36dc2b3ce64204b4da38668b3aef567abf3b9b4 Mon Sep 17 00:00:00 2001 From: SanChuan <2194167956@qq.com> Date: Thu, 5 Jan 2023 16:07:13 +0000 Subject: [PATCH 08/24] learn test --- src/test/bridges/nft_trading/ExampleE2E.t.sol | 109 ++++++++++++++++ .../bridges/nft_trading/ExampleUnit.t.sol | 119 ++++++++++++++++++ 2 files changed, 228 insertions(+) create mode 100644 src/test/bridges/nft_trading/ExampleE2E.t.sol create mode 100644 src/test/bridges/nft_trading/ExampleUnit.t.sol diff --git a/src/test/bridges/nft_trading/ExampleE2E.t.sol b/src/test/bridges/nft_trading/ExampleE2E.t.sol new file mode 100644 index 000000000..9f64f7f84 --- /dev/null +++ b/src/test/bridges/nft_trading/ExampleE2E.t.sol @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec. +pragma solidity >=0.8.4; + +import {BridgeTestBase} from "./../../aztec/base/BridgeTestBase.sol"; +import {AztecTypes} from "rollup-encoder/libraries/AztecTypes.sol"; + +// Example-specific imports +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ExampleBridge} from "../../../bridges/example/ExampleBridge.sol"; +import {ErrorLib} from "../../../bridges/base/ErrorLib.sol"; + +/** + * @notice The purpose of this test is to test the bridge in an environment that is as close to the final deployment + * as possible without spinning up all the rollup infrastructure (sequencer, proof generator etc.). + */ +contract ExampleE2ETest is BridgeTestBase { + address public constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + address private constant BENEFICIARY = address(11); + + // The reference to the example bridge + ExampleBridge internal bridge; + // To store the id of the example bridge after being added + uint256 private id; + + function setUp() public { + // Deploy a new example bridge + bridge = new ExampleBridge(address(ROLLUP_PROCESSOR)); + + // Use the label cheatcode to mark the address with "Example Bridge" in the traces + vm.label(address(bridge), "Example Bridge"); + + // Impersonate the multi-sig to add a new bridge + vm.startPrank(MULTI_SIG); + + // List the example-bridge with a gasLimit of 120k + // WARNING: If you set this value too low the interaction will fail for seemingly no reason! + // OTOH if you se it too high bridge users will pay too much + ROLLUP_PROCESSOR.setSupportedBridge(address(bridge), 120000); + + // List USDC with a gasLimit of 100k + // Note: necessary for assets which are not already registered on RollupProcessor + // Call https://etherscan.io/address/0xFF1F2B4ADb9dF6FC8eAFecDcbF96A2B351680455#readProxyContract#F25 to get + // addresses of all the listed ERC20 tokens + ROLLUP_PROCESSOR.setSupportedAsset(USDC, 100000); + + vm.stopPrank(); + + // Fetch the id of the example bridge + id = ROLLUP_PROCESSOR.getSupportedBridgesLength(); + + // Subsidize the bridge when used with USDC and register a beneficiary + AztecTypes.AztecAsset memory usdcAsset = ROLLUP_ENCODER.getRealAztecAsset(USDC); + uint256 criteria = bridge.computeCriteria(usdcAsset, emptyAsset, usdcAsset, emptyAsset, 0); + uint32 gasPerMinute = 200; + SUBSIDY.subsidize{value: 1 ether}(address(bridge), criteria, gasPerMinute); + + SUBSIDY.registerBeneficiary(BENEFICIARY); + + // Set the rollupBeneficiary on BridgeTestBase so that it gets included in the proofData + ROLLUP_ENCODER.setRollupBeneficiary(BENEFICIARY); + } + + // @dev In order to avoid overflows we set _depositAmount to be uint96 instead of uint256. + function testExampleBridgeE2ETest(uint96 _depositAmount) public { + vm.assume(_depositAmount > 1); + vm.warp(block.timestamp + 1 days); + + // Use the helper function to fetch the support AztecAsset for DAI + AztecTypes.AztecAsset memory usdcAsset = ROLLUP_ENCODER.getRealAztecAsset(address(USDC)); + + // Mint the depositAmount of Dai to rollupProcessor + deal(USDC, address(ROLLUP_PROCESSOR), _depositAmount); + + // Computes the encoded data for the specific bridge interaction + ROLLUP_ENCODER.defiInteractionL2(id, usdcAsset, emptyAsset, usdcAsset, emptyAsset, 0, _depositAmount); + + // Execute the rollup with the bridge interaction. Ensure that event as seen above is emitted. + (uint256 outputValueA, uint256 outputValueB, bool isAsync) = ROLLUP_ENCODER.processRollupAndGetBridgeResult(); + + // Note: Unlike in unit tests there is no need to manually transfer the tokens - RollupProcessor does this + + // Check the output values are as expected + assertEq(outputValueA, _depositAmount, "outputValueA doesn't equal deposit"); + assertEq(outputValueB, 0, "Non-zero outputValueB"); + assertFalse(isAsync, "Bridge is not synchronous"); + + // Check that the balance of the rollup is same as before interaction (bridge just sends funds back) + assertEq(_depositAmount, IERC20(USDC).balanceOf(address(ROLLUP_PROCESSOR)), "Balances must match"); + + // Perform a second rollup with half the deposit, perform similar checks. + uint256 secondDeposit = _depositAmount / 2; + + ROLLUP_ENCODER.defiInteractionL2(id, usdcAsset, emptyAsset, usdcAsset, emptyAsset, 0, secondDeposit); + + // Execute the rollup with the bridge interaction. Ensure that event as seen above is emitted. + (outputValueA, outputValueB, isAsync) = ROLLUP_ENCODER.processRollupAndGetBridgeResult(); + + // Check the output values are as expected + assertEq(outputValueA, secondDeposit, "outputValueA doesn't equal second deposit"); + assertEq(outputValueB, 0, "Non-zero outputValueB"); + assertFalse(isAsync, "Bridge is not synchronous"); + + // Check that the balance of the rollup is same as before interaction (bridge just sends funds back) + assertEq(_depositAmount, IERC20(USDC).balanceOf(address(ROLLUP_PROCESSOR)), "Balances must match"); + + assertGt(SUBSIDY.claimableAmount(BENEFICIARY), 0, "Claimable was not updated"); + } +} diff --git a/src/test/bridges/nft_trading/ExampleUnit.t.sol b/src/test/bridges/nft_trading/ExampleUnit.t.sol new file mode 100644 index 000000000..0c6aeb464 --- /dev/null +++ b/src/test/bridges/nft_trading/ExampleUnit.t.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec. +pragma solidity >=0.8.4; + +import {BridgeTestBase} from "./../../aztec/base/BridgeTestBase.sol"; +import {AztecTypes} from "rollup-encoder/libraries/AztecTypes.sol"; + +// Example-specific imports +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ExampleBridge} from "../../../bridges/example/ExampleBridge.sol"; +import {ErrorLib} from "../../../bridges/base/ErrorLib.sol"; + +// @notice The purpose of this test is to directly test convert functionality of the bridge. +contract ExampleUnitTest is BridgeTestBase { + address private constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; + address private constant BENEFICIARY = address(11); + + address private rollupProcessor; + // The reference to the example bridge + ExampleBridge private bridge; + + // @dev This method exists on RollupProcessor.sol. It's defined here in order to be able to receive ETH like a real + // rollup processor would. + function receiveEthFromBridge(uint256 _interactionNonce) external payable {} + + function setUp() public { + // In unit tests we set address of rollupProcessor to the address of this test contract + rollupProcessor = address(this); + + // Deploy a new example bridge + bridge = new ExampleBridge(rollupProcessor); + + // Set ETH balance of bridge and BENEFICIARY to 0 for clarity (somebody sent ETH to that address on mainnet) + vm.deal(address(bridge), 0); + vm.deal(BENEFICIARY, 0); + + // Use the label cheatcode to mark the address with "Example Bridge" in the traces + vm.label(address(bridge), "Example Bridge"); + + // Subsidize the bridge when used with Dai and register a beneficiary + AztecTypes.AztecAsset memory daiAsset = ROLLUP_ENCODER.getRealAztecAsset(DAI); + uint256 criteria = bridge.computeCriteria(daiAsset, emptyAsset, daiAsset, emptyAsset, 0); + uint32 gasPerMinute = 200; + SUBSIDY.subsidize{value: 1 ether}(address(bridge), criteria, gasPerMinute); + + SUBSIDY.registerBeneficiary(BENEFICIARY); + } + + function testInvalidCaller(address _callerAddress) public { + vm.assume(_callerAddress != rollupProcessor); + // Use HEVM cheatcode to call from a different address than is address(this) + vm.prank(_callerAddress); + vm.expectRevert(ErrorLib.InvalidCaller.selector); + bridge.convert(emptyAsset, emptyAsset, emptyAsset, emptyAsset, 0, 0, 0, address(0)); + } + + function testInvalidInputAssetType() public { + vm.expectRevert(ErrorLib.InvalidInputA.selector); + bridge.convert(emptyAsset, emptyAsset, emptyAsset, emptyAsset, 0, 0, 0, address(0)); + } + + function testInvalidOutputAssetType() public { + AztecTypes.AztecAsset memory inputAssetA = + AztecTypes.AztecAsset({id: 1, erc20Address: DAI, assetType: AztecTypes.AztecAssetType.ERC20}); + vm.expectRevert(ErrorLib.InvalidOutputA.selector); + bridge.convert(inputAssetA, emptyAsset, emptyAsset, emptyAsset, 0, 0, 0, address(0)); + } + + function testExampleBridgeUnitTestFixed() public { + testExampleBridgeUnitTest(10 ether); + } + + // @notice The purpose of this test is to directly test convert functionality of the bridge. + // @dev In order to avoid overflows we set _depositAmount to be uint96 instead of uint256. + function testExampleBridgeUnitTest(uint96 _depositAmount) public { + vm.warp(block.timestamp + 1 days); + + // Define input and output assets + AztecTypes.AztecAsset memory inputAssetA = + AztecTypes.AztecAsset({id: 1, erc20Address: DAI, assetType: AztecTypes.AztecAssetType.ERC20}); + + AztecTypes.AztecAsset memory outputAssetA = inputAssetA; + + // Rollup processor transfers ERC20 tokens to the bridge before calling convert. Since we are calling + // bridge.convert(...) function directly we have to transfer the funds in the test on our own. In this case + // we'll solve it by directly minting the _depositAmount of Dai to the bridge. + deal(DAI, address(bridge), _depositAmount); + + // Store dai balance before interaction to be able to verify the balance after interaction is correct + uint256 daiBalanceBefore = IERC20(DAI).balanceOf(rollupProcessor); + + (uint256 outputValueA, uint256 outputValueB, bool isAsync) = bridge.convert( + inputAssetA, // _inputAssetA - definition of an input asset + emptyAsset, // _inputAssetB - not used so can be left empty + outputAssetA, // _outputAssetA - in this example equal to input asset + emptyAsset, // _outputAssetB - not used so can be left empty + _depositAmount, // _totalInputValue - an amount of input asset A sent to the bridge + 0, // _interactionNonce + 0, // _auxData - not used in the example bridge + BENEFICIARY // _rollupBeneficiary - address, the subsidy will be sent to + ); + + // Now we transfer the funds back from the bridge to the rollup processor + // In this case input asset equals output asset so I only work with the input asset definition + // Basically in all the real world use-cases output assets would differ from input assets + IERC20(inputAssetA.erc20Address).transferFrom(address(bridge), rollupProcessor, outputValueA); + + assertEq(outputValueA, _depositAmount, "Output value A doesn't equal deposit amount"); + assertEq(outputValueB, 0, "Output value B is not 0"); + assertTrue(!isAsync, "Bridge is incorrectly in an async mode"); + + uint256 daiBalanceAfter = IERC20(DAI).balanceOf(rollupProcessor); + + assertEq(daiBalanceAfter - daiBalanceBefore, _depositAmount, "Balances must match"); + + SUBSIDY.withdraw(BENEFICIARY); + assertGt(BENEFICIARY.balance, 0, "Subsidy was not claimed"); + } +} From 28d812432ed921944a9cde85f8a3871f82682bf5 Mon Sep 17 00:00:00 2001 From: SanChuan <2194167956@qq.com> Date: Fri, 6 Jan 2023 03:20:57 +0000 Subject: [PATCH 09/24] update specs flow --- specs/bridges/nft_trading/readme.md | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/specs/bridges/nft_trading/readme.md b/specs/bridges/nft_trading/readme.md index 1e226df85..85d463c89 100644 --- a/specs/bridges/nft_trading/readme.md +++ b/specs/bridges/nft_trading/readme.md @@ -7,28 +7,36 @@ In this way, the owner of one NFT owner is in private with the power of Aztec's ## What protocol(s) does the bridge interact with ? - - The bridge interacts with [OpenSea](https://opensea.io/). ## What is the flow of the bridge? -The simple flow as below: + +The NFT bridge will design as a Wrap Erc721 Contract, that means everytime when the contract received a NFT, it will mint a relevant Wrapped NFT to this user, This Wrapped NFT can be transfer to Aztec L2, also can be unwrap and redeem the original NFT anytimes. + +The `BUY` flow as below: 1. User A on Aztec chain bridge interface pay and buy an NFT on Opensea in Ethereum. 2. The "Buy" order and relevant fee is send to L1 by Aztec's rollup. 3. The bridge contract in L1 is triggered to interact with OpenSea protocol. -4. Then the bridge contract own this NFT and record it to a private user id. -5. The User A On Aztec can redeem this NFT anytime, with his valid signature. - +4. Then the bridge contract own this NFT and Mint a Wrapped NFT for Aztec L2 to bridge. +5. The User A On Aztec Owned this Wrapped NFT. +The `REDEEM` flow as below: +1. User A hold an wrapped NFT in Aztec L2. +2. He call the bridge to unwrapped and send the real NFT to a L1 address. +3. The Bridge contract is trigged, and send the resl NFT to the user specifed address, and burn the wrapped NFT. ### General Properties of convert(...) function + The `AztecTypes.AztecAsset calldata _inputAssetA` should be specificed as the Bridge's wrapped NFT. + And the relevant function calling is encoded into the `_auxData` Info. + In the Bridge contract, the function will be routed by the decoded +`_auxData`. - The bridge is synchronous, and will always return `isAsync = false`. - The bridge uses `_auxData` to encode the target NFT, id, price. -- The Bridge perform token pre-approvals to allow the `ROLLUP_PROCESSOR` and `UNI_ROUTER` to pull tokens from it. - This is to reduce gas-overhead when performing the actions. It is safe to do, as the bridge is not holding the funds itself. +- The Bridge perform token pre-approvals to allow the `ROLLUP_PROCESSOR` to pull tokens from it. + ## Is the contract upgradeable? From 8582acdac39d384beaa4a2f5f8e0b1967c65da77 Mon Sep 17 00:00:00 2001 From: sc <2194167956@qq.com> Date: Fri, 6 Jan 2023 22:45:49 +0800 Subject: [PATCH 10/24] add NFT Transfer templete Refers to: https://github.com/critesjosh/aztec-connect-starter/tree/nft-bridge Kudo to this guy. --- .gitignore | 3 +- src/bridges/nft-basic/NFTVault.sol | 135 +++++++++ src/bridges/nft_trading/NFTVault.sol | 135 +++++++++ src/bridges/registry/AddressRegistry.sol | 87 ++++++ .../nft-basic/NFTVaultDeployment.s.sol | 42 +++ .../registry/AddressRegistryDeployment.s.sol | 28 ++ .../bridges/nft-basic/NFTVaultBasicE2E.t.sol | 157 +++++++++++ .../bridges/nft-basic/NFTVaultBasicUnit.t.sol | 257 ++++++++++++++++++ .../bridges/registry/AddressRegistryE2E.t.sol | 76 ++++++ .../registry/AddressRegistryUnitTest.t.sol | 127 +++++++++ 10 files changed, 1046 insertions(+), 1 deletion(-) create mode 100644 src/bridges/nft-basic/NFTVault.sol create mode 100644 src/bridges/nft_trading/NFTVault.sol create mode 100644 src/bridges/registry/AddressRegistry.sol create mode 100644 src/deployment/nft-basic/NFTVaultDeployment.s.sol create mode 100644 src/deployment/registry/AddressRegistryDeployment.s.sol create mode 100644 src/test/bridges/nft-basic/NFTVaultBasicE2E.t.sol create mode 100644 src/test/bridges/nft-basic/NFTVaultBasicUnit.t.sol create mode 100644 src/test/bridges/registry/AddressRegistryE2E.t.sol create mode 100644 src/test/bridges/registry/AddressRegistryUnitTest.t.sol diff --git a/.gitignore b/.gitignore index 4b6e85fbc..3d60e9478 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ out/ node_modules/ yarn-error.log typechain-types/ -broadcast/ \ No newline at end of file +broadcast/ +compose-dev.yaml diff --git a/src/bridges/nft-basic/NFTVault.sol b/src/bridges/nft-basic/NFTVault.sol new file mode 100644 index 000000000..dec468119 --- /dev/null +++ b/src/bridges/nft-basic/NFTVault.sol @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec. +pragma solidity >=0.8.4; + +import {IERC721} from "../../../lib/openzeppelin-contracts/contracts/interfaces/IERC721.sol"; +import {AztecTypes} from "../../../lib/rollup-encoder/src/libraries/AztecTypes.sol"; +import {ErrorLib} from "../base/ErrorLib.sol"; +import {BridgeBase} from "../base/BridgeBase.sol"; +import {AddressRegistry} from "../registry/AddressRegistry.sol"; + +/** + * @title Basic NFT Vault for Aztec. + * @author Josh Crites, (@critesjosh on Github), Aztec Team + * @notice You can use this contract to hold your NFTs on Aztec. Whoever holds the corresponding virutal asset note can withdraw the NFT. + * @dev This bridge demonstrates basic functionality for an NFT bridge. This may be extended to support more features. + */ +contract NFTVault is BridgeBase { + struct NFTAsset { + address collection; + uint256 tokenId; + } + + AddressRegistry public immutable REGISTRY; + + mapping(uint256 => NFTAsset) public nftAssets; + + error InvalidVirtualAssetId(); + + event NFTDeposit(uint256 indexed virtualAssetId, address indexed collection, uint256 indexed tokenId); + event NFTWithdraw(uint256 indexed virtualAssetId, address indexed collection, uint256 indexed tokenId); + + /** + * @notice Set the addresses of RollupProcessor and AddressRegistry + * @param _rollupProcessor Address of the RollupProcessor + * @param _registry Address of the AddressRegistry + */ + constructor(address _rollupProcessor, address _registry) BridgeBase(_rollupProcessor) { + REGISTRY = AddressRegistry(_registry); + } + + /** + * @notice Function for the first step of a NFT deposit, a NFT withdrawal, or transfer to another NFTVault. + * @dev This method can only be called from the RollupProcessor. The first step of the + * deposit flow returns a virutal asset note that will represent the NFT on Aztec. After the + * virutal asset note is received on Aztec, the user calls matchAndPull which deposits the NFT + * into Aztec and matches it with the virtual asset. When the virutal asset is sent to this function + * it is burned and the NFT is sent to the recipient passed in _auxData. + * + * @param _inputAssetA - ETH (Deposit) or VIRTUAL (Withdrawal) + * @param _outputAssetA - VIRTUAL (Deposit) or 0 ETH (Withdrawal) + * @param _totalInputValue - must be 1 wei (Deposit) or 1 VIRTUAL (Withdrawal) + * @param _interactionNonce - A globally unique identifier of this interaction/`convert(...)` call + * corresponding to the returned virtual asset id + * @param _auxData - corresponds to the Ethereum address id in the AddressRegistry.sol for withdrawals + * @return outputValueA - 1 VIRTUAL asset (Deposit) or 0 ETH (Withdrawal) + * + */ + + function convert( + AztecTypes.AztecAsset calldata _inputAssetA, + AztecTypes.AztecAsset calldata, + AztecTypes.AztecAsset calldata _outputAssetA, + AztecTypes.AztecAsset calldata, + uint256 _totalInputValue, + uint256 _interactionNonce, + uint64 _auxData, + address + ) + external + payable + override (BridgeBase) + onlyRollup + returns (uint256 outputValueA, uint256 outputValueB, bool isAsync) + { + if ( + _inputAssetA.assetType == AztecTypes.AztecAssetType.NOT_USED + || _inputAssetA.assetType == AztecTypes.AztecAssetType.ERC20 + ) revert ErrorLib.InvalidInputA(); + if ( + _outputAssetA.assetType == AztecTypes.AztecAssetType.NOT_USED + || _outputAssetA.assetType == AztecTypes.AztecAssetType.ERC20 + ) revert ErrorLib.InvalidOutputA(); + if (_totalInputValue != 1) { + revert ErrorLib.InvalidInputAmount(); + } + if ( + _inputAssetA.assetType == AztecTypes.AztecAssetType.ETH + && _outputAssetA.assetType == AztecTypes.AztecAssetType.VIRTUAL + ) { + return (1, 0, false); + } else if (_inputAssetA.assetType == AztecTypes.AztecAssetType.VIRTUAL) { + NFTAsset memory token = nftAssets[_inputAssetA.id]; + if (token.collection == address(0x0)) { + revert ErrorLib.InvalidInputA(); + } + + address to = REGISTRY.addresses(_auxData); + if (to == address(0x0)) { + revert ErrorLib.InvalidAuxData(); + } + delete nftAssets[_inputAssetA.id]; + emit NFTWithdraw(_inputAssetA.id, token.collection, token.tokenId); + + if (_outputAssetA.assetType == AztecTypes.AztecAssetType.ETH) { + IERC721(token.collection).transferFrom(address(this), to, token.tokenId); + return (0, 0, false); + } else { + IERC721(token.collection).approve(to, token.tokenId); + NFTVault(to).matchAndPull(_interactionNonce, token.collection, token.tokenId); + return (1, 0, false); + } + } + } + + /** + * @notice Function for the second step of a NFT deposit or for transfers from other NFTVaults. + * @dev For a deposit, this method is called by an Ethereum L1 account that owns the NFT to deposit. + * The user must approve this bridge contract to transfer the users NFT before this function + * is called. This function assumes the NFT contract complies with the ERC721 standard. + * For a transfer from another NFTVault, this method is called by the NFTVault that is sending the NFT. + * + * @param _virtualAssetId - the virutal asset id of the note returned in the deposit step of the convert function + * @param _collection - collection address of the NFT + * @param _tokenId - the token id of the NFT + */ + + function matchAndPull(uint256 _virtualAssetId, address _collection, uint256 _tokenId) external { + if (nftAssets[_virtualAssetId].collection != address(0x0)) { + revert InvalidVirtualAssetId(); + } + nftAssets[_virtualAssetId] = NFTAsset({collection: _collection, tokenId: _tokenId}); + IERC721(_collection).transferFrom(msg.sender, address(this), _tokenId); + emit NFTDeposit(_virtualAssetId, _collection, _tokenId); + } +} diff --git a/src/bridges/nft_trading/NFTVault.sol b/src/bridges/nft_trading/NFTVault.sol new file mode 100644 index 000000000..dec468119 --- /dev/null +++ b/src/bridges/nft_trading/NFTVault.sol @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec. +pragma solidity >=0.8.4; + +import {IERC721} from "../../../lib/openzeppelin-contracts/contracts/interfaces/IERC721.sol"; +import {AztecTypes} from "../../../lib/rollup-encoder/src/libraries/AztecTypes.sol"; +import {ErrorLib} from "../base/ErrorLib.sol"; +import {BridgeBase} from "../base/BridgeBase.sol"; +import {AddressRegistry} from "../registry/AddressRegistry.sol"; + +/** + * @title Basic NFT Vault for Aztec. + * @author Josh Crites, (@critesjosh on Github), Aztec Team + * @notice You can use this contract to hold your NFTs on Aztec. Whoever holds the corresponding virutal asset note can withdraw the NFT. + * @dev This bridge demonstrates basic functionality for an NFT bridge. This may be extended to support more features. + */ +contract NFTVault is BridgeBase { + struct NFTAsset { + address collection; + uint256 tokenId; + } + + AddressRegistry public immutable REGISTRY; + + mapping(uint256 => NFTAsset) public nftAssets; + + error InvalidVirtualAssetId(); + + event NFTDeposit(uint256 indexed virtualAssetId, address indexed collection, uint256 indexed tokenId); + event NFTWithdraw(uint256 indexed virtualAssetId, address indexed collection, uint256 indexed tokenId); + + /** + * @notice Set the addresses of RollupProcessor and AddressRegistry + * @param _rollupProcessor Address of the RollupProcessor + * @param _registry Address of the AddressRegistry + */ + constructor(address _rollupProcessor, address _registry) BridgeBase(_rollupProcessor) { + REGISTRY = AddressRegistry(_registry); + } + + /** + * @notice Function for the first step of a NFT deposit, a NFT withdrawal, or transfer to another NFTVault. + * @dev This method can only be called from the RollupProcessor. The first step of the + * deposit flow returns a virutal asset note that will represent the NFT on Aztec. After the + * virutal asset note is received on Aztec, the user calls matchAndPull which deposits the NFT + * into Aztec and matches it with the virtual asset. When the virutal asset is sent to this function + * it is burned and the NFT is sent to the recipient passed in _auxData. + * + * @param _inputAssetA - ETH (Deposit) or VIRTUAL (Withdrawal) + * @param _outputAssetA - VIRTUAL (Deposit) or 0 ETH (Withdrawal) + * @param _totalInputValue - must be 1 wei (Deposit) or 1 VIRTUAL (Withdrawal) + * @param _interactionNonce - A globally unique identifier of this interaction/`convert(...)` call + * corresponding to the returned virtual asset id + * @param _auxData - corresponds to the Ethereum address id in the AddressRegistry.sol for withdrawals + * @return outputValueA - 1 VIRTUAL asset (Deposit) or 0 ETH (Withdrawal) + * + */ + + function convert( + AztecTypes.AztecAsset calldata _inputAssetA, + AztecTypes.AztecAsset calldata, + AztecTypes.AztecAsset calldata _outputAssetA, + AztecTypes.AztecAsset calldata, + uint256 _totalInputValue, + uint256 _interactionNonce, + uint64 _auxData, + address + ) + external + payable + override (BridgeBase) + onlyRollup + returns (uint256 outputValueA, uint256 outputValueB, bool isAsync) + { + if ( + _inputAssetA.assetType == AztecTypes.AztecAssetType.NOT_USED + || _inputAssetA.assetType == AztecTypes.AztecAssetType.ERC20 + ) revert ErrorLib.InvalidInputA(); + if ( + _outputAssetA.assetType == AztecTypes.AztecAssetType.NOT_USED + || _outputAssetA.assetType == AztecTypes.AztecAssetType.ERC20 + ) revert ErrorLib.InvalidOutputA(); + if (_totalInputValue != 1) { + revert ErrorLib.InvalidInputAmount(); + } + if ( + _inputAssetA.assetType == AztecTypes.AztecAssetType.ETH + && _outputAssetA.assetType == AztecTypes.AztecAssetType.VIRTUAL + ) { + return (1, 0, false); + } else if (_inputAssetA.assetType == AztecTypes.AztecAssetType.VIRTUAL) { + NFTAsset memory token = nftAssets[_inputAssetA.id]; + if (token.collection == address(0x0)) { + revert ErrorLib.InvalidInputA(); + } + + address to = REGISTRY.addresses(_auxData); + if (to == address(0x0)) { + revert ErrorLib.InvalidAuxData(); + } + delete nftAssets[_inputAssetA.id]; + emit NFTWithdraw(_inputAssetA.id, token.collection, token.tokenId); + + if (_outputAssetA.assetType == AztecTypes.AztecAssetType.ETH) { + IERC721(token.collection).transferFrom(address(this), to, token.tokenId); + return (0, 0, false); + } else { + IERC721(token.collection).approve(to, token.tokenId); + NFTVault(to).matchAndPull(_interactionNonce, token.collection, token.tokenId); + return (1, 0, false); + } + } + } + + /** + * @notice Function for the second step of a NFT deposit or for transfers from other NFTVaults. + * @dev For a deposit, this method is called by an Ethereum L1 account that owns the NFT to deposit. + * The user must approve this bridge contract to transfer the users NFT before this function + * is called. This function assumes the NFT contract complies with the ERC721 standard. + * For a transfer from another NFTVault, this method is called by the NFTVault that is sending the NFT. + * + * @param _virtualAssetId - the virutal asset id of the note returned in the deposit step of the convert function + * @param _collection - collection address of the NFT + * @param _tokenId - the token id of the NFT + */ + + function matchAndPull(uint256 _virtualAssetId, address _collection, uint256 _tokenId) external { + if (nftAssets[_virtualAssetId].collection != address(0x0)) { + revert InvalidVirtualAssetId(); + } + nftAssets[_virtualAssetId] = NFTAsset({collection: _collection, tokenId: _tokenId}); + IERC721(_collection).transferFrom(msg.sender, address(this), _tokenId); + emit NFTDeposit(_virtualAssetId, _collection, _tokenId); + } +} diff --git a/src/bridges/registry/AddressRegistry.sol b/src/bridges/registry/AddressRegistry.sol new file mode 100644 index 000000000..cc3b68043 --- /dev/null +++ b/src/bridges/registry/AddressRegistry.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec. +pragma solidity >=0.8.4; + +import {AztecTypes} from "../../../lib/rollup-encoder/src/libraries/AztecTypes.sol"; +import {ErrorLib} from "../base/ErrorLib.sol"; +import {BridgeBase} from "../base/BridgeBase.sol"; + +/** + * @title Aztec Address Registry. + * @author Josh Crites (@critesjosh on Github), Aztec team + * @notice This contract can be used to anonymously register an ethereum address with an id. + * This is useful for reducing the amount of data required to pass an ethereum address through auxData. + * @dev Use this contract to lookup ethereum addresses by id. + */ +contract AddressRegistry is BridgeBase { + uint256 public addressCount; + mapping(uint256 => address) public addresses; + + event AddressRegistered(uint256 indexed index, address indexed entity); + + /** + * @notice Set address of rollup processor + * @param _rollupProcessor Address of rollup processor + */ + constructor(address _rollupProcessor) BridgeBase(_rollupProcessor) {} + + /** + * @notice Function for getting VIRTUAL assets (step 1) to register an address and registering an address (step 2). + * @dev This method can only be called from the RollupProcessor. The first step to register an address is for a user to + * get the type(uint160).max value of VIRTUAL assets back from the bridge. The second step is for the user + * to send an amount of VIRTUAL assets back to the bridge. The amount that is sent back is equal to the number of the + * ethereum address that is being registered (e.g. uint160(0x2e782B05290A7fFfA137a81a2bad2446AD0DdFEB)). + * + * @param _inputAssetA - ETH (step 1) or VIRTUAL (step 2) + * @param _outputAssetA - VIRTUAL (steps 1 and 2) + * @param _totalInputValue - must be 1 wei (ETH) (step 1) or address value (step 2) + * @return outputValueA - type(uint160).max (step 1) or 0 VIRTUAL (step 2) + * + */ + + function convert( + AztecTypes.AztecAsset calldata _inputAssetA, + AztecTypes.AztecAsset calldata, + AztecTypes.AztecAsset calldata _outputAssetA, + AztecTypes.AztecAsset calldata, + uint256 _totalInputValue, + uint256, + uint64, + address + ) external payable override (BridgeBase) onlyRollup returns (uint256 outputValueA, uint256, bool) { + if ( + _inputAssetA.assetType == AztecTypes.AztecAssetType.NOT_USED + || _inputAssetA.assetType == AztecTypes.AztecAssetType.ERC20 + ) revert ErrorLib.InvalidInputA(); + if (_outputAssetA.assetType != AztecTypes.AztecAssetType.VIRTUAL) { + revert ErrorLib.InvalidOutputA(); + } + if (_inputAssetA.assetType == AztecTypes.AztecAssetType.ETH) { + if (_totalInputValue != 1) { + revert ErrorLib.InvalidInputAmount(); + } + return (type(uint160).max, 0, false); + } else if (_inputAssetA.assetType == AztecTypes.AztecAssetType.VIRTUAL) { + address toRegister = address(uint160(_totalInputValue)); + registerAddress(toRegister); + return (0, 0, false); + } + } + + /** + * @notice Register an address at the registry + * @dev This function can be called directly from another Ethereum account. This can be done in + * one step, in one transaction. Coming from Ethereum directly, this method is not as privacy + * preserving as registering an address through the bridge. + * + * @param _to - The address to register + * @return addressCount - the index of address that has been registered + */ + + function registerAddress(address _to) public returns (uint256) { + uint256 userIndex = addressCount++; + addresses[userIndex] = _to; + emit AddressRegistered(userIndex, _to); + return userIndex; + } +} diff --git a/src/deployment/nft-basic/NFTVaultDeployment.s.sol b/src/deployment/nft-basic/NFTVaultDeployment.s.sol new file mode 100644 index 000000000..9d3e7dfe4 --- /dev/null +++ b/src/deployment/nft-basic/NFTVaultDeployment.s.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec. +pragma solidity >=0.8.4; + +import {BaseDeployment} from "../base/BaseDeployment.s.sol"; +import {NFTVault} from "../../bridges/nft-basic/NFTVault.sol"; +import {AddressRegistry} from "../../bridges/registry/AddressRegistry.sol"; + +contract NFTVaultDeployment is BaseDeployment { + function deploy(address _addressRegistry) public returns (address) { + emit log("Deploying NFTVault bridge"); + + vm.broadcast(); + NFTVault bridge = new NFTVault(ROLLUP_PROCESSOR, _addressRegistry); + + emit log_named_address("NFTVault bridge deployed to", address(bridge)); + + return address(bridge); + } + + function deployAndList(address _addressRegistry) public returns (address) { + address bridge = deploy(_addressRegistry); + + uint256 addressId = listBridge(bridge, 135500); + emit log_named_uint("NFTVault bridge address id", addressId); + + return bridge; + } + + function deployAndListAddressRegistry() public returns (address) { + emit log("Deploying AddressRegistry bridge"); + + AddressRegistry bridge = new AddressRegistry(ROLLUP_PROCESSOR); + + emit log_named_address("AddressRegistry bridge deployed to", address(bridge)); + + uint256 addressId = listBridge(address(bridge), 120500); + emit log_named_uint("AddressRegistry bridge address id", addressId); + + return address(bridge); + } +} diff --git a/src/deployment/registry/AddressRegistryDeployment.s.sol b/src/deployment/registry/AddressRegistryDeployment.s.sol new file mode 100644 index 000000000..52cd58b6b --- /dev/null +++ b/src/deployment/registry/AddressRegistryDeployment.s.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec. +pragma solidity >=0.8.4; + +import {BaseDeployment} from "../base/BaseDeployment.s.sol"; +import {AddressRegistry} from "../../bridges/registry/AddressRegistry.sol"; + +contract AddressRegistryDeployment is BaseDeployment { + function deploy() public returns (address) { + emit log("Deploying AddressRegistry bridge"); + + vm.broadcast(); + AddressRegistry bridge = new AddressRegistry(ROLLUP_PROCESSOR); + + emit log_named_address("AddressRegistry bridge deployed to", address(bridge)); + + return address(bridge); + } + + function deployAndList() public returns (address) { + address bridge = deploy(); + + uint256 addressId = listBridge(bridge, 120500); + emit log_named_uint("AddressRegistry bridge address id", addressId); + + return bridge; + } +} diff --git a/src/test/bridges/nft-basic/NFTVaultBasicE2E.t.sol b/src/test/bridges/nft-basic/NFTVaultBasicE2E.t.sol new file mode 100644 index 000000000..6858227e8 --- /dev/null +++ b/src/test/bridges/nft-basic/NFTVaultBasicE2E.t.sol @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec. +pragma solidity >=0.8.4; + +import {BridgeTestBase} from "./../../aztec/base/BridgeTestBase.sol"; +import {AztecTypes} from "rollup-encoder/libraries/AztecTypes.sol"; + +// Example-specific imports +import {NFTVault} from "../../../bridges/nft-basic/NFTVault.sol"; +import {AddressRegistry} from "../../../bridges/registry/AddressRegistry.sol"; +import {ErrorLib} from "../../../bridges/base/ErrorLib.sol"; +import {ERC721PresetMinterPauserAutoId} from + "@openzeppelin/contracts/token/ERC721/presets/ERC721PresetMinterPauserAutoId.sol"; + +/** + * @notice The purpose of this test is to test the bridge in an environment that is as close to the final deployment + * as possible without spinning up all the rollup infrastructure (sequencer, proof generator etc.). + */ +contract NFTVaultBasicE2ETest is BridgeTestBase { + NFTVault internal bridge; + NFTVault internal bridge2; + AddressRegistry private registry; + ERC721PresetMinterPauserAutoId private nftContract; + + // To store the id of the bridge after being added + uint256 private bridgeId; + uint256 private bridge2Id; + uint256 private registryBridgeId; + uint256 private tokenIdToDeposit = 1; + address private constant REGISTER_ADDRESS = 0x2e782B05290A7fFfA137a81a2bad2446AD0DdFEA; + address private constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; + AztecTypes.AztecAsset private ethAsset; + AztecTypes.AztecAsset private virtualAsset1 = + AztecTypes.AztecAsset({id: 1, erc20Address: address(0), assetType: AztecTypes.AztecAssetType.VIRTUAL}); + AztecTypes.AztecAsset private virtualAsset100 = + AztecTypes.AztecAsset({id: 100, erc20Address: address(0), assetType: AztecTypes.AztecAssetType.VIRTUAL}); + AztecTypes.AztecAsset private erc20InputAsset = + AztecTypes.AztecAsset({id: 1, erc20Address: DAI, assetType: AztecTypes.AztecAssetType.ERC20}); + + event NFTDeposit(uint256 indexed virtualAssetId, address indexed collection, uint256 indexed tokenId); + event NFTWithdraw(uint256 indexed virtualAssetId, address indexed collection, uint256 indexed tokenId); + + function setUp() public { + registry = new AddressRegistry(address(ROLLUP_PROCESSOR)); + bridge = new NFTVault(address(ROLLUP_PROCESSOR), address(registry)); + bridge2 = new NFTVault(address(ROLLUP_PROCESSOR), address(registry)); + nftContract = new ERC721PresetMinterPauserAutoId("test", "NFT", ""); + nftContract.mint(address(this)); + nftContract.mint(address(this)); + nftContract.mint(address(this)); + + nftContract.approve(address(bridge), 0); + nftContract.approve(address(bridge), 1); + nftContract.approve(address(bridge), 2); + + ethAsset = ROLLUP_ENCODER.getRealAztecAsset(address(0)); + + vm.label(address(registry), "AddressRegistry Bridge"); + vm.label(address(bridge), "NFTVault Bridge"); + + // Impersonate the multi-sig to add a new bridge + vm.startPrank(MULTI_SIG); + + // WARNING: If you set this value too low the interaction will fail for seemingly no reason! + // OTOH if you se it too high bridge users will pay too much + ROLLUP_PROCESSOR.setSupportedBridge(address(registry), 120500); + ROLLUP_PROCESSOR.setSupportedBridge(address(bridge), 135500); + ROLLUP_PROCESSOR.setSupportedBridge(address(bridge2), 135500); + + vm.stopPrank(); + + // Fetch the id of the bridges + registryBridgeId = ROLLUP_PROCESSOR.getSupportedBridgesLength() - 2; + bridgeId = ROLLUP_PROCESSOR.getSupportedBridgesLength() - 1; + bridge2Id = ROLLUP_PROCESSOR.getSupportedBridgesLength(); + // get virtual assets to register an address + ROLLUP_ENCODER.defiInteractionL2(registryBridgeId, ethAsset, emptyAsset, virtualAsset1, emptyAsset, 0, 1); + ROLLUP_ENCODER.processRollup(); + // get virtual assets to register 2nd NFTVault + ROLLUP_ENCODER.defiInteractionL2(registryBridgeId, ethAsset, emptyAsset, virtualAsset1, emptyAsset, 0, 1); + ROLLUP_ENCODER.processRollup(); + + // register an address + uint160 inputAmount = uint160(REGISTER_ADDRESS); + ROLLUP_ENCODER.defiInteractionL2( + registryBridgeId, virtualAsset1, emptyAsset, virtualAsset1, emptyAsset, 0, inputAmount + ); + ROLLUP_ENCODER.processRollup(); + + // register 2nd NFTVault in AddressRegistry + uint160 bridge2AddressAmount = uint160(address(bridge2)); + ROLLUP_ENCODER.defiInteractionL2( + registryBridgeId, virtualAsset1, emptyAsset, virtualAsset1, emptyAsset, 0, bridge2AddressAmount + ); + } + + function testDeposit() public { + // get virtual asset before deposit + ROLLUP_ENCODER.defiInteractionL2(bridgeId, ethAsset, emptyAsset, virtualAsset100, emptyAsset, 0, 1); + + (uint256 outputValueA, uint256 outputValueB, bool isAsync) = ROLLUP_ENCODER.processRollupAndGetBridgeResult(); + + assertEq(outputValueA, 1, "Output value A doesn't equal 1"); + assertEq(outputValueB, 0, "Output value B is not 0"); + assertTrue(!isAsync, "Bridge is incorrectly in an async mode"); + + address collection = address(nftContract); + + vm.expectEmit(true, true, true, false); + emit NFTDeposit(virtualAsset100.id, collection, tokenIdToDeposit); + bridge.matchAndPull(virtualAsset100.id, collection, tokenIdToDeposit); + (address returnedCollection, uint256 returnedId) = bridge.nftAssets(virtualAsset100.id); + assertEq(returnedId, tokenIdToDeposit, "nft token id does not match input"); + assertEq(returnedCollection, collection, "collection data does not match"); + } + + function testWithdraw() public { + testDeposit(); + uint64 auxData = uint64(registry.addressCount() - 2); + + vm.expectEmit(true, true, false, false); + emit NFTWithdraw(virtualAsset100.id, address(nftContract), tokenIdToDeposit); + ROLLUP_ENCODER.defiInteractionL2(bridgeId, virtualAsset100, emptyAsset, ethAsset, emptyAsset, auxData, 1); + + (uint256 outputValueA, uint256 outputValueB, bool isAsync) = ROLLUP_ENCODER.processRollupAndGetBridgeResult(); + address owner = nftContract.ownerOf(tokenIdToDeposit); + assertEq(REGISTER_ADDRESS, owner, "registered address is not the owner"); + assertEq(outputValueA, 0, "Output value A is not 0"); + assertEq(outputValueB, 0, "Output value B is not 0"); + assertTrue(!isAsync, "Bridge is incorrectly in an async mode"); + + (address _a, uint256 _id) = bridge.nftAssets(virtualAsset100.id); + assertEq(_a, address(0), "collection address is not 0"); + assertEq(_id, 0, "token id is not 0"); + } + + function testTransfer() public { + testDeposit(); + (address collection, uint256 tokenId) = bridge.nftAssets(virtualAsset100.id); + uint64 auxData = uint64(registry.addressCount() - 1); + + uint256 interactionNonce = ROLLUP_ENCODER.getNextNonce(); + + vm.expectEmit(true, true, true, false, address(bridge)); + emit NFTWithdraw(virtualAsset100.id, collection, tokenId); + vm.expectEmit(true, true, true, false, address(bridge2)); + emit NFTDeposit(interactionNonce, collection, tokenId); + ROLLUP_ENCODER.defiInteractionL2(bridgeId, virtualAsset100, emptyAsset, virtualAsset1, emptyAsset, auxData, 1); + + ROLLUP_ENCODER.processRollup(); + + // check that the nft was transferred to the second NFTVault + (address returnedCollection, uint256 returnedId) = bridge2.nftAssets(interactionNonce); + assertEq(returnedId, tokenIdToDeposit, "nft token id does not match input"); + assertEq(returnedCollection, collection, "collection data does not match"); + } +} diff --git a/src/test/bridges/nft-basic/NFTVaultBasicUnit.t.sol b/src/test/bridges/nft-basic/NFTVaultBasicUnit.t.sol new file mode 100644 index 000000000..7fdf5ce43 --- /dev/null +++ b/src/test/bridges/nft-basic/NFTVaultBasicUnit.t.sol @@ -0,0 +1,257 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec. +pragma solidity >=0.8.4; + +import {BridgeTestBase} from "./../../aztec/base/BridgeTestBase.sol"; +import {AztecTypes} from "rollup-encoder/libraries/AztecTypes.sol"; + +// Example-specific imports +import {ERC721PresetMinterPauserAutoId} from + "@openzeppelin/contracts/token/ERC721/presets/ERC721PresetMinterPauserAutoId.sol"; +import {NFTVault} from "../../../bridges/nft-basic/NFTVault.sol"; +import {ErrorLib} from "../../../bridges/base/ErrorLib.sol"; +import {AddressRegistry} from "../../../bridges/registry/AddressRegistry.sol"; + +// @notice The purpose of this test is to directly test convert functionality of the bridge. +contract NFTVaultBasicUnitTest is BridgeTestBase { + struct NftAsset { + address collection; + uint256 id; + } + + address private rollupProcessor; + + NFTVault private bridge; + NFTVault private bridge2; + ERC721PresetMinterPauserAutoId private nftContract; + uint256 private tokenIdToDeposit = 1; + AddressRegistry private registry; + address private constant REGISTER_ADDRESS = 0x2e782B05290A7fFfA137a81a2bad2446AD0DdFEA; + address private constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; + + AztecTypes.AztecAsset private ethAsset = + AztecTypes.AztecAsset({id: 0, erc20Address: address(0), assetType: AztecTypes.AztecAssetType.ETH}); + AztecTypes.AztecAsset private virtualAsset1 = + AztecTypes.AztecAsset({id: 1, erc20Address: address(0), assetType: AztecTypes.AztecAssetType.VIRTUAL}); + AztecTypes.AztecAsset private virtualAsset100 = + AztecTypes.AztecAsset({id: 100, erc20Address: address(0), assetType: AztecTypes.AztecAssetType.VIRTUAL}); + AztecTypes.AztecAsset private erc20InputAsset = + AztecTypes.AztecAsset({id: 1, erc20Address: DAI, assetType: AztecTypes.AztecAssetType.ERC20}); + + // @dev This method exists on RollupProcessor.sol. It's defined here in order to be able to receive ETH like a real + // rollup processor would. + function receiveEthFromBridge(uint256 _interactionNonce) external payable {} + + function setUp() public { + // In unit tests we set address of rollupProcessor to the address of this test contract + rollupProcessor = address(this); + + registry = new AddressRegistry(rollupProcessor); + bridge = new NFTVault(rollupProcessor, address(registry)); + bridge2 = new NFTVault(rollupProcessor, address(registry)); + nftContract = new ERC721PresetMinterPauserAutoId("test", "NFT", ""); + nftContract.mint(address(this)); + nftContract.mint(address(this)); + nftContract.mint(address(this)); + + nftContract.approve(address(bridge), 0); + nftContract.approve(address(bridge), 1); + nftContract.approve(address(bridge), 2); + + _registerAddress(REGISTER_ADDRESS); + _registerAddress(address(bridge2)); + + // Set ETH balance of bridge to 0 for clarity (somebody sent ETH to that address on mainnet) + vm.deal(address(bridge), 0); + vm.label(address(bridge), "Basic NFT Vault Bridge"); + } + + function testInvalidCaller(address _callerAddress) public { + vm.assume(_callerAddress != rollupProcessor); + // Use HEVM cheatcode to call from a different address than is address(this) + vm.prank(_callerAddress); + vm.expectRevert(ErrorLib.InvalidCaller.selector); + bridge.convert(emptyAsset, emptyAsset, emptyAsset, emptyAsset, 0, 0, 0, address(0)); + } + + function testInvalidInputAssetType() public { + vm.expectRevert(ErrorLib.InvalidInputA.selector); + bridge.convert(emptyAsset, emptyAsset, emptyAsset, emptyAsset, 0, 0, 0, address(0)); + } + + function testInvalidOutputAssetType() public { + vm.expectRevert(ErrorLib.InvalidOutputA.selector); + bridge.convert(ethAsset, emptyAsset, erc20InputAsset, emptyAsset, 0, 0, 0, address(0)); + } + + function testGetVirtualAssetUnitTest() public { + vm.warp(block.timestamp + 1 days); + + (uint256 outputValueA, uint256 outputValueB, bool isAsync) = bridge.convert( + ethAsset, // _inputAssetA + emptyAsset, // _inputAssetB + virtualAsset100, // _outputAssetA + emptyAsset, // _outputAssetB + 1, // _totalInputValue + 0, // _interactionNonce + 0, // _auxData + address(0) // _rollupBeneficiary + ); + + assertEq(outputValueA, 1, "Output value A doesn't equal 1"); + assertEq(outputValueB, 0, "Output value B is not 0"); + assertTrue(!isAsync, "Bridge is incorrectly in an async mode"); + } + + // should fail because sending more than 1 wei + function testGetVirtualAssetShouldFail() public { + vm.warp(block.timestamp + 1 days); + + vm.expectRevert(); + bridge.convert( + ethAsset, // _inputAssetA + emptyAsset, // _inputAssetB + virtualAsset100, // _outputAssetA + emptyAsset, // _outputAssetB + 2, // _totalInputValue + 0, // _interactionNonce + 0, // _auxData + address(0) // _rollupBeneficiary + ); + } + + function testDeposit() public { + vm.warp(block.timestamp + 1 days); + + address collection = address(nftContract); + bridge.matchAndPull(virtualAsset100.id, collection, tokenIdToDeposit); + (address returnedCollection, uint256 returnedId) = bridge.nftAssets(virtualAsset100.id); + assertEq(returnedId, tokenIdToDeposit, "nft token id does not match input"); + assertEq(returnedCollection, collection, "collection data does not match"); + } + + // should fail because an NFT with this id has already been deposited + function testDepositFailWithDuplicateNft() public { + testDeposit(); + vm.warp(block.timestamp + 1 days); + + address collection = address(nftContract); + vm.expectRevert(); + bridge.matchAndPull(virtualAsset100.id, collection, tokenIdToDeposit); + } + + // should fail because no withdraw address has been registered with this id + function testWithdrawUnregisteredWithdrawAddress() public { + testDeposit(); + uint64 auxData = 1000; + vm.expectRevert(ErrorLib.InvalidAuxData.selector); + bridge.convert( + virtualAsset100, // _inputAssetA + emptyAsset, // _inputAssetB + ethAsset, // _outputAssetA + emptyAsset, // _outputAssetB + 1, // _totalInputValue + 0, // _interactionNonce + auxData, + address(0) + ); + } + + function testWithdraw() public { + testDeposit(); + uint64 auxData = uint64(registry.addressCount() - 2); + (uint256 outputValueA, uint256 outputValueB, bool isAsync) = bridge.convert( + virtualAsset100, // _inputAssetA + emptyAsset, // _inputAssetB + ethAsset, // _outputAssetA + emptyAsset, // _outputAssetB + 1, // _totalInputValue + 0, // _interactionNonce + auxData, // _auxData + address(0) + ); + address owner = nftContract.ownerOf(tokenIdToDeposit); + assertEq(REGISTER_ADDRESS, owner, "registered address is not the owner"); + assertEq(outputValueA, 0, "Output value A is not 0"); + assertEq(outputValueB, 0, "Output value B is not 0"); + assertTrue(!isAsync, "Bridge is incorrectly in an async mode"); + + (address _a, uint256 _id) = bridge.nftAssets(virtualAsset100.id); + assertEq(_a, address(0), "collection address is not 0"); + assertEq(_id, 0, "token id is not 0"); + } + + // should fail because no NFT has been registered with this virtual asset + function testWithdrawUnregisteredNft() public { + testDeposit(); + uint64 auxData = uint64(registry.addressCount()); + vm.expectRevert(ErrorLib.InvalidInputA.selector); + bridge.convert( + virtualAsset1, // _inputAssetA + emptyAsset, // _inputAssetB + ethAsset, // _outputAssetA + emptyAsset, // _outputAssetB + 1, // _totalInputValue + 0, // _interactionNonce + auxData, + address(0) + ); + } + + function testTransfer() public { + testDeposit(); + uint64 auxData = uint64(registry.addressCount() - 1); + uint256 interactionNonce = 128; + + (uint256 outputValueA, uint256 outputValueB, bool isAsync) = bridge.convert( + virtualAsset100, // _inputAssetA + emptyAsset, // _inputAssetB + virtualAsset100, // _outputAssetA + emptyAsset, // _outputAssetB + 1, // _totalInputValue + interactionNonce, // _interactionNonce + auxData, // _auxData + address(0) + ); + address owner = nftContract.ownerOf(tokenIdToDeposit); + assertEq(address(bridge2), owner, "registered address is not the owner"); + assertEq(outputValueA, 1, "Output value A is not 1"); + assertEq(outputValueB, 0, "Output value B is not 0"); + assertTrue(!isAsync, "Bridge is incorrectly in an async mode"); + + // test that the nft was deleted from bridge 1 + (address bridge1collection,) = bridge.nftAssets(virtualAsset100.id); + assertEq(bridge1collection, address(0), "collection was not deleted"); + + // test that the nft was added to bridge 2 + (address _a, uint256 _id) = bridge2.nftAssets(interactionNonce); + assertEq(_a, address(nftContract), "collection address is not 0"); + assertEq(_id, tokenIdToDeposit, "token id is not 0"); + } + + function _registerAddress(address _addressToRegister) internal { + // get virtual assets + registry.convert( + ethAsset, + emptyAsset, + virtualAsset1, + emptyAsset, + 1, // _totalInputValue + 0, // _interactionNonce + 0, // _auxData + address(0x0) + ); + uint256 inputAmount = uint160(address(_addressToRegister)); + // register an address + registry.convert( + virtualAsset1, + emptyAsset, + virtualAsset1, + emptyAsset, + inputAmount, + 0, // _interactionNonce + 0, // _auxData + address(0x0) + ); + } +} diff --git a/src/test/bridges/registry/AddressRegistryE2E.t.sol b/src/test/bridges/registry/AddressRegistryE2E.t.sol new file mode 100644 index 000000000..2191ac198 --- /dev/null +++ b/src/test/bridges/registry/AddressRegistryE2E.t.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec. +pragma solidity >=0.8.4; + +import {BridgeTestBase} from "./../../aztec/base/BridgeTestBase.sol"; +import {AztecTypes} from "../../../../lib/rollup-encoder/src/libraries/AztecTypes.sol"; + +// Example-specific imports +import {AddressRegistry} from "../../../bridges/registry/AddressRegistry.sol"; +import {ErrorLib} from "../../../bridges/base/ErrorLib.sol"; + +/** + * @notice The purpose of this test is to test the bridge in an environment that is as close to the final deployment + * as possible without spinning up all the rollup infrastructure (sequencer, proof generator etc.). + */ +contract AddressRegistryE2ETest is BridgeTestBase { + AddressRegistry internal bridge; + uint256 private id; + AztecTypes.AztecAsset private ethAsset; + AztecTypes.AztecAsset private virtualAsset1; + uint256 public maxInt = type(uint160).max; + + event AddressRegistered(uint256 indexed addressCount, address indexed registeredAddress); + + function setUp() public { + bridge = new AddressRegistry(address(ROLLUP_PROCESSOR)); + ethAsset = ROLLUP_ENCODER.getRealAztecAsset(address(0)); + virtualAsset1 = + AztecTypes.AztecAsset({id: 0, erc20Address: address(0), assetType: AztecTypes.AztecAssetType.VIRTUAL}); + + vm.label(address(bridge), "Address Registry Bridge"); + + // Impersonate the multi-sig to add a new bridge + vm.startPrank(MULTI_SIG); + + // WARNING: If you set this value too low the interaction will fail for seemingly no reason! + // OTOH if you se it too high bridge users will pay too much + ROLLUP_PROCESSOR.setSupportedBridge(address(bridge), 120000); + + vm.stopPrank(); + + // Fetch the id of the example bridge + id = ROLLUP_PROCESSOR.getSupportedBridgesLength(); + } + + function testGetVirtualAssets() public { + vm.warp(block.timestamp + 1 days); + + ROLLUP_ENCODER.defiInteractionL2(id, ethAsset, emptyAsset, virtualAsset1, emptyAsset, 0, 1); + + (uint256 outputValueA, uint256 outputValueB, bool isAsync) = ROLLUP_ENCODER.processRollupAndGetBridgeResult(); + + assertEq(outputValueA, maxInt, "outputValueA doesn't equal maxInt"); + assertEq(outputValueB, 0, "Non-zero outputValueB"); + assertFalse(isAsync, "Bridge is not synchronous"); + } + + function testRegistration() public { + uint160 inputAmount = uint160(0x2e782B05290A7fFfA137a81a2bad2446AD0DdFEA); + + vm.expectEmit(true, true, false, false); + emit AddressRegistered(0, address(inputAmount)); + + ROLLUP_ENCODER.defiInteractionL2(id, virtualAsset1, emptyAsset, virtualAsset1, emptyAsset, 0, inputAmount); + + (uint256 outputValueA, uint256 outputValueB, bool isAsync) = ROLLUP_ENCODER.processRollupAndGetBridgeResult(); + + uint64 addressId = uint64(bridge.addressCount()) - 1; + address newlyRegistered = bridge.addresses(addressId); + + assertEq(address(inputAmount), newlyRegistered, "input amount doesn't equal newly registered address"); + assertEq(outputValueA, 0, "Non-zero outputValueA"); + assertEq(outputValueB, 0, "Non-zero outputValueB"); + assertFalse(isAsync, "Bridge is not synchronous"); + } +} diff --git a/src/test/bridges/registry/AddressRegistryUnitTest.t.sol b/src/test/bridges/registry/AddressRegistryUnitTest.t.sol new file mode 100644 index 000000000..a66fcd274 --- /dev/null +++ b/src/test/bridges/registry/AddressRegistryUnitTest.t.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec. +pragma solidity >=0.8.4; + +import {BridgeTestBase} from "./../../aztec/base/BridgeTestBase.sol"; +import {AztecTypes} from "rollup-encoder/libraries/AztecTypes.sol"; + +// Example-specific imports +import {AddressRegistry} from "../../../bridges/registry/AddressRegistry.sol"; +import {ErrorLib} from "../../../bridges/base/ErrorLib.sol"; + +// @notice The purpose of this test is to directly test convert functionality of the bridge. +contract AddressRegistryUnitTest is BridgeTestBase { + address private constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; + address private rollupProcessor; + AddressRegistry private bridge; + uint256 public maxInt = type(uint160).max; + AztecTypes.AztecAsset private ethAsset = + AztecTypes.AztecAsset({id: 0, erc20Address: address(0), assetType: AztecTypes.AztecAssetType.ETH}); + AztecTypes.AztecAsset private virtualAsset = + AztecTypes.AztecAsset({id: 0, erc20Address: address(0), assetType: AztecTypes.AztecAssetType.VIRTUAL}); + AztecTypes.AztecAsset private daiAsset = + AztecTypes.AztecAsset({id: 1, erc20Address: DAI, assetType: AztecTypes.AztecAssetType.ERC20}); + + event AddressRegistered(uint256 indexed addressCount, address indexed registeredAddress); + + // @dev This method exists on RollupProcessor.sol. It's defined here in order to be able to receive ETH like a real + // rollup processor would. + function receiveEthFromBridge(uint256 _interactionNonce) external payable {} + + function setUp() public { + // In unit tests we set address of rollupProcessor to the address of this test contract + rollupProcessor = address(this); + + bridge = new AddressRegistry(rollupProcessor); + + // Use the label cheatcode to mark the address with "AddressRegistry Bridge" in the traces + vm.label(address(bridge), "AddressRegistry Bridge"); + } + + function testInvalidCaller(address _callerAddress) public { + vm.assume(_callerAddress != rollupProcessor); + // Use HEVM cheatcode to call from a different address than is address(this) + vm.prank(_callerAddress); + vm.expectRevert(ErrorLib.InvalidCaller.selector); + bridge.convert(emptyAsset, emptyAsset, emptyAsset, emptyAsset, 0, 0, 0, address(0)); + } + + function testInvalidInputAssetType() public { + vm.expectRevert(ErrorLib.InvalidInputA.selector); + bridge.convert(daiAsset, emptyAsset, emptyAsset, emptyAsset, 0, 0, 0, address(0)); + } + + function testInvalidOutputAssetType() public { + vm.expectRevert(ErrorLib.InvalidOutputA.selector); + bridge.convert(ethAsset, emptyAsset, daiAsset, emptyAsset, 0, 0, 0, address(0)); + } + + function testInvalidInputAmount() public { + vm.expectRevert(ErrorLib.InvalidInputAmount.selector); + + bridge.convert( + ethAsset, + emptyAsset, + virtualAsset, + emptyAsset, + 0, // _totalInputValue + 0, // _interactionNonce + 0, // _auxData + address(0x0) + ); + } + + function testGetBackMaxVirtualAssets() public { + vm.warp(block.timestamp + 1 days); + + (uint256 outputValueA, uint256 outputValueB, bool isAsync) = bridge.convert( + ethAsset, + emptyAsset, + virtualAsset, + emptyAsset, + 1, // _totalInputValue + 0, // _interactionNonce + 0, // _auxData + address(0x0) + ); + + assertEq(outputValueA, maxInt, "Output value A doesn't equal maxInt"); + assertEq(outputValueB, 0, "Output value B is not 0"); + assertTrue(!isAsync, "Bridge is incorrectly in an async mode"); + } + + function testRegistringAnAddress() public { + vm.warp(block.timestamp + 1 days); + + uint160 inputAmount = uint160(0x2e782B05290A7fFfA137a81a2bad2446AD0DdFEA); + + vm.expectEmit(true, true, false, false); + emit AddressRegistered(0, address(inputAmount)); + + (uint256 outputValueA, uint256 outputValueB, bool isAsync) = bridge.convert( + virtualAsset, + emptyAsset, + virtualAsset, + emptyAsset, + inputAmount, // _totalInputValue + 0, // _interactionNonce + 0, // _auxData + address(0x0) + ); + + uint256 id = bridge.addressCount() - 1; + address newlyRegistered = bridge.addresses(id); + + assertEq(address(inputAmount), newlyRegistered, "Address not registered"); + assertEq(outputValueA, 0, "Output value is not 0"); + assertEq(outputValueB, 0, "Output value B is not 0"); + assertTrue(!isAsync, "Bridge is incorrectly in an async mode"); + } + + function testRegisterFromEth() public { + address to = address(0x2e782B05290A7fFfA137a81a2bad2446AD0DdFEA); + uint256 count = bridge.registerAddress(to); + address registered = bridge.addresses(count); + assertEq(to, registered, "Address not registered"); + } +} From 2b3e1987778a09f29e77fa2fd1f6a818e9d76cd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Bene=C5=A1?= Date: Mon, 9 Jan 2023 09:30:37 -0600 Subject: [PATCH 11/24] style: updated formatting (#306) * style: updated formatting * fix: Bump liquity gas bounds Co-authored-by: LHerskind --- foundry.toml | 3 --- src/aztec/Subsidy.sol | 25 ++++++++----------- src/bridges/angle/AngleSLPBridge.sol | 4 +-- src/bridges/base/BridgeBase.sol | 4 +-- src/bridges/curve/CurveStEthBridge.sol | 2 +- src/bridges/dca/BiDCABridge.sol | 4 +-- src/bridges/dca/UniswapDCABridge.sol | 2 +- src/bridges/donation/DonationBridge.sol | 2 +- src/bridges/element/ElementBridge.sol | 4 +-- src/bridges/erc4626/ERC4626Bridge.sol | 4 +-- src/bridges/example/ExampleBridge.sol | 4 +-- src/bridges/lido/LidoBridge.sol | 2 +- src/bridges/liquity/StabilityPoolBridge.sol | 4 +-- src/bridges/liquity/StakingBridge.sol | 4 +-- src/bridges/liquity/TroveBridge.sol | 6 ++--- src/bridges/uniswap/UniswapBridge.sol | 4 +-- src/bridges/yearn/YearnBridge.sol | 4 +-- .../liquity/LiquityTroveDeployment.s.sol | 2 +- src/gas/angle/AngleSLPGas.s.sol | 2 +- src/gas/erc4626/ERC4626Gas.s.sol | 2 +- src/gas/liquity/TroveBridgeGas.s.sol | 2 +- src/gas/uniswap/UniswapGas.s.sol | 2 +- src/test/bridges/erc4626/mocks/WETHVault.sol | 2 +- src/test/bridges/liquity/TroveBridgeE2E.t.sol | 6 ++--- .../bridges/liquity/utils/MockPriceFeed.sol | 2 +- 25 files changed, 47 insertions(+), 55 deletions(-) diff --git a/foundry.toml b/foundry.toml index 31ad78d1b..4e0dedb99 100644 --- a/foundry.toml +++ b/foundry.toml @@ -11,7 +11,4 @@ ffi = true optimizer = true optimizer_runs = 100000 -[fmt] -variable_override_spacing=false - # See more config options https://github.com/gakonst/foundry/tree/master/config diff --git a/src/aztec/Subsidy.sol b/src/aztec/Subsidy.sol index 55213e72b..4d9bf2a78 100644 --- a/src/aztec/Subsidy.sol +++ b/src/aztec/Subsidy.sol @@ -73,7 +73,7 @@ contract Subsidy is ISubsidy { * @param _beneficiary The address of the beneficiary to query * @return The amount of claimable ETH for `_beneficiary` */ - function claimableAmount(address _beneficiary) external view override (ISubsidy) returns (uint256) { + function claimableAmount(address _beneficiary) external view override(ISubsidy) returns (uint256) { return beneficiaries[_beneficiary].claimable; } @@ -82,7 +82,7 @@ contract Subsidy is ISubsidy { * @param _beneficiary The address of the beneficiary to check * @return True if the `_beneficiary` is registered, false otherwise */ - function isRegistered(address _beneficiary) external view override (ISubsidy) returns (bool) { + function isRegistered(address _beneficiary) external view override(ISubsidy) returns (bool) { return beneficiaries[_beneficiary].registered; } @@ -92,12 +92,7 @@ contract Subsidy is ISubsidy { * @param _criteria The criteria of the subsidy * @return The subsidy data object */ - function getSubsidy(address _bridge, uint256 _criteria) - external - view - override (ISubsidy) - returns (Subsidy memory) - { + function getSubsidy(address _bridge, uint256 _criteria) external view override(ISubsidy) returns (Subsidy memory) { return subsidies[_bridge][_criteria]; } @@ -114,7 +109,7 @@ contract Subsidy is ISubsidy { uint256[] calldata _criteria, uint32[] calldata _gasUsage, uint32[] calldata _minGasPerMinute - ) external override (ISubsidy) { + ) external override(ISubsidy) { uint256 criteriasLength = _criteria.length; if (criteriasLength != _gasUsage.length || criteriasLength != _minGasPerMinute.length) { revert ArrayLengthsDoNotMatch(); @@ -137,7 +132,7 @@ contract Subsidy is ISubsidy { * IDefiBridge.convert(...) function to be as predictable as possible. If the cost is too variable users would * overpay since RollupProcessor works with constant gas limits. */ - function registerBeneficiary(address _beneficiary) external override (ISubsidy) { + function registerBeneficiary(address _beneficiary) external override(ISubsidy) { beneficiaries[_beneficiary].registered = true; emit BeneficiaryRegistered(_beneficiary); } @@ -152,7 +147,7 @@ contract Subsidy is ISubsidy { * 3) subsidy.gasUsage not set: `subsidy.gasUsage` == 0, * 4) ETH value sent too low: `msg.value` < `MIN_SUBSIDY_VALUE`. */ - function subsidize(address _bridge, uint256 _criteria, uint32 _gasPerMinute) external payable override (ISubsidy) { + function subsidize(address _bridge, uint256 _criteria, uint32 _gasPerMinute) external payable override(ISubsidy) { if (msg.value < MIN_SUBSIDY_VALUE) { revert SubsidyTooLow(); } @@ -185,7 +180,7 @@ contract Subsidy is ISubsidy { * @param _criteria A value defining the specific bridge call to subsidize * @dev Reverts if `available` is 0. */ - function topUp(address _bridge, uint256 _criteria) external payable override (ISubsidy) { + function topUp(address _bridge, uint256 _criteria) external payable override(ISubsidy) { // Caching subsidy in order to minimize number of SLOADs and SSTOREs Subsidy memory sub = subsidies[_bridge][_criteria]; @@ -207,7 +202,7 @@ contract Subsidy is ISubsidy { * @param _beneficiary Address which is going to receive the subsidy * @return subsidy ETH amount which was added to the `_beneficiary` claimable balance */ - function claimSubsidy(uint256 _criteria, address _beneficiary) external override (ISubsidy) returns (uint256) { + function claimSubsidy(uint256 _criteria, address _beneficiary) external override(ISubsidy) returns (uint256) { if (_beneficiary == address(0)) { return 0; } @@ -250,7 +245,7 @@ contract Subsidy is ISubsidy { * @param _beneficiary Address which is going to receive the subsidy * @return - ETH amount which was sent to the `_beneficiary` */ - function withdraw(address _beneficiary) external override (ISubsidy) returns (uint256) { + function withdraw(address _beneficiary) external override(ISubsidy) returns (uint256) { uint256 withdrawableBalance = beneficiaries[_beneficiary].claimable; // Immediately updating the balance to avoid re-entrancy attack beneficiaries[_beneficiary].claimable = 0; @@ -284,7 +279,7 @@ contract Subsidy is ISubsidy { */ function setGasUsageAndMinGasPerMinute(uint256 _criteria, uint32 _gasUsage, uint32 _minGasPerMinute) public - override (ISubsidy) + override(ISubsidy) { // Loading `sub` first in order to not overwrite `sub.available` in case this function was already called // and a subsidy was set. diff --git a/src/bridges/angle/AngleSLPBridge.sol b/src/bridges/angle/AngleSLPBridge.sol index c75c333e3..7b8f1ad00 100644 --- a/src/bridges/angle/AngleSLPBridge.sol +++ b/src/bridges/angle/AngleSLPBridge.sol @@ -89,7 +89,7 @@ contract AngleSLPBridge is BridgeBase { uint256 _interactionNonce, uint64 _auxData, address _rollupBeneficiary - ) external payable override (BridgeBase) onlyRollup returns (uint256 outputValueA, uint256, bool) { + ) external payable override(BridgeBase) onlyRollup returns (uint256 outputValueA, uint256, bool) { address inputAssetA; address outputAssetA; @@ -144,7 +144,7 @@ contract AngleSLPBridge is BridgeBase { AztecTypes.AztecAsset calldata, AztecTypes.AztecAsset calldata, uint64 _auxData - ) public pure override (BridgeBase) returns (uint256) { + ) public pure override(BridgeBase) returns (uint256) { if (_auxData > 1) { revert ErrorLib.InvalidAuxData(); } diff --git a/src/bridges/base/BridgeBase.sol b/src/bridges/base/BridgeBase.sol index 21a227700..235dc01bb 100644 --- a/src/bridges/base/BridgeBase.sol +++ b/src/bridges/base/BridgeBase.sol @@ -39,7 +39,7 @@ abstract contract BridgeBase is IDefiBridge { uint256, uint64, address - ) external payable virtual override (IDefiBridge) returns (uint256, uint256, bool) { + ) external payable virtual override(IDefiBridge) returns (uint256, uint256, bool) { revert MissingImplementation(); } @@ -50,7 +50,7 @@ abstract contract BridgeBase is IDefiBridge { AztecTypes.AztecAsset calldata, uint256, uint64 - ) external payable virtual override (IDefiBridge) returns (uint256, uint256, bool) { + ) external payable virtual override(IDefiBridge) returns (uint256, uint256, bool) { revert ErrorLib.AsyncDisabled(); } diff --git a/src/bridges/curve/CurveStEthBridge.sol b/src/bridges/curve/CurveStEthBridge.sol index 99c9019e5..4beda802d 100644 --- a/src/bridges/curve/CurveStEthBridge.sol +++ b/src/bridges/curve/CurveStEthBridge.sol @@ -71,7 +71,7 @@ contract CurveStEthBridge is BridgeBase { uint256 _interactionNonce, uint64 _auxData, address - ) external payable override (BridgeBase) onlyRollup returns (uint256 outputValueA, uint256, bool) { + ) external payable override(BridgeBase) onlyRollup returns (uint256 outputValueA, uint256, bool) { bool isETHInput = _inputAssetA.assetType == AztecTypes.AztecAssetType.ETH; bool isWstETHInput = _inputAssetA.assetType == AztecTypes.AztecAssetType.ERC20 && _inputAssetA.erc20Address == address(WRAPPED_STETH); diff --git a/src/bridges/dca/BiDCABridge.sol b/src/bridges/dca/BiDCABridge.sol index d798db9fc..b8701aab8 100644 --- a/src/bridges/dca/BiDCABridge.sol +++ b/src/bridges/dca/BiDCABridge.sol @@ -243,7 +243,7 @@ abstract contract BiDCABridge is BridgeBase { uint256 _interactionNonce, uint64 _numTicks, address - ) external payable override (BridgeBase) onlyRollup returns (uint256, uint256, bool) { + ) external payable override(BridgeBase) onlyRollup returns (uint256, uint256, bool) { address inputAssetAddress = _inputAssetA.erc20Address; address outputAssetAddress = _outputAssetA.erc20Address; @@ -289,7 +289,7 @@ abstract contract BiDCABridge is BridgeBase { external payable virtual - override (BridgeBase) + override(BridgeBase) onlyRollup returns (uint256 outputValueA, uint256, bool interactionComplete) { diff --git a/src/bridges/dca/UniswapDCABridge.sol b/src/bridges/dca/UniswapDCABridge.sol index 6d6ebcf7f..b4e0a2c02 100644 --- a/src/bridges/dca/UniswapDCABridge.sol +++ b/src/bridges/dca/UniswapDCABridge.sol @@ -109,7 +109,7 @@ contract UniswapDCABridge is BiDCABridge { * @dev Reverts if the price is stale or negative * @return Price */ - function getPrice() public virtual override (BiDCABridge) returns (uint256) { + function getPrice() public virtual override(BiDCABridge) returns (uint256) { (, int256 answer,,,) = ORACLE.latestRoundData(); if (answer < 0) { revert NegativePrice(); diff --git a/src/bridges/donation/DonationBridge.sol b/src/bridges/donation/DonationBridge.sol index ef106e66e..da274e89f 100644 --- a/src/bridges/donation/DonationBridge.sol +++ b/src/bridges/donation/DonationBridge.sol @@ -57,7 +57,7 @@ contract DonationBridge is BridgeBase { uint256, uint64 _auxData, address - ) external payable override (BridgeBase) onlyRollup returns (uint256, uint256, bool) { + ) external payable override(BridgeBase) onlyRollup returns (uint256, uint256, bool) { address receiver = donees[_auxData]; if (receiver == address(0)) { diff --git a/src/bridges/element/ElementBridge.sol b/src/bridges/element/ElementBridge.sol index a703b890e..2a7b5603b 100644 --- a/src/bridges/element/ElementBridge.sol +++ b/src/bridges/element/ElementBridge.sol @@ -428,7 +428,7 @@ contract ElementBridge is BridgeBase { ) external payable - override (BridgeBase) + override(BridgeBase) onlyRollup returns (uint256 outputValueA, uint256 outputValueB, bool isAsync) { @@ -602,7 +602,7 @@ contract ElementBridge is BridgeBase { ) external payable - override (BridgeBase) + override(BridgeBase) onlyRollup returns (uint256 outputValueA, uint256 outputValueB, bool interactionCompleted) { diff --git a/src/bridges/erc4626/ERC4626Bridge.sol b/src/bridges/erc4626/ERC4626Bridge.sol index 2e9bea983..47d7e372c 100644 --- a/src/bridges/erc4626/ERC4626Bridge.sol +++ b/src/bridges/erc4626/ERC4626Bridge.sol @@ -87,7 +87,7 @@ contract ERC4626Bridge is BridgeBase { uint256 _interactionNonce, uint64 _auxData, address _rollupBeneficiary - ) external payable override (BridgeBase) onlyRollup returns (uint256 outputValueA, uint256, bool) { + ) external payable override(BridgeBase) onlyRollup returns (uint256 outputValueA, uint256, bool) { address inputToken = _inputAssetA.erc20Address; address outputToken = _outputAssetA.erc20Address; @@ -131,7 +131,7 @@ contract ERC4626Bridge is BridgeBase { AztecTypes.AztecAsset calldata _outputAssetA, AztecTypes.AztecAsset calldata, uint64 - ) public pure override (BridgeBase) returns (uint256) { + ) public pure override(BridgeBase) returns (uint256) { return _computeCriteria(_inputAssetA.erc20Address, _outputAssetA.erc20Address); } diff --git a/src/bridges/example/ExampleBridge.sol b/src/bridges/example/ExampleBridge.sol index 227f57655..0ee7aed18 100644 --- a/src/bridges/example/ExampleBridge.sol +++ b/src/bridges/example/ExampleBridge.sol @@ -59,7 +59,7 @@ contract ExampleBridge is BridgeBase { uint256, uint64 _auxData, address _rollupBeneficiary - ) external payable override (BridgeBase) onlyRollup returns (uint256 outputValueA, uint256, bool) { + ) external payable override(BridgeBase) onlyRollup returns (uint256 outputValueA, uint256, bool) { // Check the input asset is ERC20 if (_inputAssetA.assetType != AztecTypes.AztecAssetType.ERC20) revert ErrorLib.InvalidInputA(); if (_outputAssetA.erc20Address != _inputAssetA.erc20Address) revert ErrorLib.InvalidOutputA(); @@ -86,7 +86,7 @@ contract ExampleBridge is BridgeBase { AztecTypes.AztecAsset calldata _outputAssetA, AztecTypes.AztecAsset calldata, uint64 - ) public view override (BridgeBase) returns (uint256) { + ) public view override(BridgeBase) returns (uint256) { return uint256(keccak256(abi.encodePacked(_inputAssetA.erc20Address, _outputAssetA.erc20Address))); } } diff --git a/src/bridges/lido/LidoBridge.sol b/src/bridges/lido/LidoBridge.sol index ea4b3404b..a3e29db3a 100644 --- a/src/bridges/lido/LidoBridge.sol +++ b/src/bridges/lido/LidoBridge.sol @@ -56,7 +56,7 @@ contract LidoBridge is BridgeBase { uint256 _interactionNonce, uint64, address - ) external payable override (BridgeBase) onlyRollup returns (uint256 outputValueA, uint256, bool isAsync) { + ) external payable override(BridgeBase) onlyRollup returns (uint256 outputValueA, uint256, bool isAsync) { bool isETHInput = _inputAssetA.assetType == AztecTypes.AztecAssetType.ETH; bool isWstETHInput = _inputAssetA.assetType == AztecTypes.AztecAssetType.ERC20 && _inputAssetA.erc20Address == address(WRAPPED_STETH); diff --git a/src/bridges/liquity/StabilityPoolBridge.sol b/src/bridges/liquity/StabilityPoolBridge.sol index ef33c1706..fb9e1e4e8 100644 --- a/src/bridges/liquity/StabilityPoolBridge.sol +++ b/src/bridges/liquity/StabilityPoolBridge.sol @@ -113,7 +113,7 @@ contract StabilityPoolBridge is BridgeBase, ERC20("StabilityPoolBridge", "SPB") uint256, uint64 _auxData, address - ) external payable override (BridgeBase) onlyRollup returns (uint256 outputValueA, uint256, bool) { + ) external payable override(BridgeBase) onlyRollup returns (uint256 outputValueA, uint256, bool) { if (_inputAssetA.erc20Address == LUSD && _outputAssetA.erc20Address == address(this)) { // Deposit // Provides LUSD to the pool and claim rewards. @@ -151,7 +151,7 @@ contract StabilityPoolBridge is BridgeBase, ERC20("StabilityPoolBridge", "SPB") /** * @dev See {IERC20-totalSupply}. */ - function totalSupply() public view override (ERC20) returns (uint256) { + function totalSupply() public view override(ERC20) returns (uint256) { return super.totalSupply() - DUST; } diff --git a/src/bridges/liquity/StakingBridge.sol b/src/bridges/liquity/StakingBridge.sol index f32fc44c3..3fbd3ec70 100644 --- a/src/bridges/liquity/StakingBridge.sol +++ b/src/bridges/liquity/StakingBridge.sol @@ -104,7 +104,7 @@ contract StakingBridge is BridgeBase, ERC20("StakingBridge", "SB") { uint256, uint64 _auxData, address - ) external payable override (BridgeBase) onlyRollup returns (uint256 outputValueA, uint256, bool) { + ) external payable override(BridgeBase) onlyRollup returns (uint256 outputValueA, uint256, bool) { if (_inputAssetA.erc20Address == LQTY && _outputAssetA.erc20Address == address(this)) { // Deposit // Stake and claim rewards @@ -141,7 +141,7 @@ contract StakingBridge is BridgeBase, ERC20("StakingBridge", "SB") { /** * @dev See {IERC20-totalSupply}. */ - function totalSupply() public view override (ERC20) returns (uint256) { + function totalSupply() public view override(ERC20) returns (uint256) { return super.totalSupply() - DUST; } diff --git a/src/bridges/liquity/TroveBridge.sol b/src/bridges/liquity/TroveBridge.sol index de8fbe235..79daf18d5 100644 --- a/src/bridges/liquity/TroveBridge.sol +++ b/src/bridges/liquity/TroveBridge.sol @@ -193,7 +193,7 @@ contract TroveBridge is BridgeBase, ERC20, Ownable, IUniswapV3SwapCallback { uint256 _interactionNonce, uint64 _auxData, address _rollupBeneficiary - ) external payable override (BridgeBase) onlyRollup returns (uint256 outputValueA, uint256 outputValueB, bool) { + ) external payable override(BridgeBase) onlyRollup returns (uint256 outputValueA, uint256 outputValueB, bool) { Status troveStatus = Status(TROVE_MANAGER.getTroveStatus(address(this))); uint256 subsidyCriteria; @@ -283,7 +283,7 @@ contract TroveBridge is BridgeBase, ERC20, Ownable, IUniswapV3SwapCallback { // @dev See _repayWithCollateral(...) method for more information about how this callback is entered. function uniswapV3SwapCallback(int256 _amount0Delta, int256 _amount1Delta, bytes calldata _data) external - override (IUniswapV3SwapCallback) + override(IUniswapV3SwapCallback) { // Swaps entirely within 0-liquidity regions are not supported if (_amount0Delta <= 0 && _amount1Delta <= 0) revert InvalidDeltaAmounts(); @@ -353,7 +353,7 @@ contract TroveBridge is BridgeBase, ERC20, Ownable, IUniswapV3SwapCallback { /** * @dev See {IERC20-totalSupply}. */ - function totalSupply() public view override (ERC20) returns (uint256) { + function totalSupply() public view override(ERC20) returns (uint256) { return super.totalSupply() - DUST; } diff --git a/src/bridges/uniswap/UniswapBridge.sol b/src/bridges/uniswap/UniswapBridge.sol index e1c6acebd..5c3bdc6c5 100644 --- a/src/bridges/uniswap/UniswapBridge.sol +++ b/src/bridges/uniswap/UniswapBridge.sol @@ -187,7 +187,7 @@ contract UniswapBridge is BridgeBase { uint256 _interactionNonce, uint64 _auxData, address _rollupBeneficiary - ) external payable override (BridgeBase) onlyRollup returns (uint256 outputValueA, uint256, bool) { + ) external payable override(BridgeBase) onlyRollup returns (uint256 outputValueA, uint256, bool) { // Accumulate subsidy to _rollupBeneficiary SUBSIDY.claimSubsidy( _computeCriteria(_inputAssetA.erc20Address, _outputAssetA.erc20Address), _rollupBeneficiary @@ -319,7 +319,7 @@ contract UniswapBridge is BridgeBase { AztecTypes.AztecAsset calldata _outputAssetA, AztecTypes.AztecAsset calldata, uint64 - ) public pure override (BridgeBase) returns (uint256) { + ) public pure override(BridgeBase) returns (uint256) { return _computeCriteria(_inputAssetA.erc20Address, _outputAssetA.erc20Address); } diff --git a/src/bridges/yearn/YearnBridge.sol b/src/bridges/yearn/YearnBridge.sol index 04c9db651..c0d80e01d 100644 --- a/src/bridges/yearn/YearnBridge.sol +++ b/src/bridges/yearn/YearnBridge.sol @@ -109,7 +109,7 @@ contract YearnBridge is BridgeBase { uint256 _interactionNonce, uint64 _auxData, address _rollupBeneficiary - ) external payable override (BridgeBase) onlyRollup returns (uint256 outputValueA, uint256, bool) { + ) external payable override(BridgeBase) onlyRollup returns (uint256 outputValueA, uint256, bool) { if (_auxData == 0) { if (_outputAssetA.assetType != AztecTypes.AztecAssetType.ERC20) { revert ErrorLib.InvalidOutputA(); @@ -192,7 +192,7 @@ contract YearnBridge is BridgeBase { AztecTypes.AztecAsset calldata, AztecTypes.AztecAsset calldata, uint64 _auxData - ) public pure override (BridgeBase) returns (uint256) { + ) public pure override(BridgeBase) returns (uint256) { if (_auxData > 1) { revert ErrorLib.InvalidAuxData(); } diff --git a/src/deployment/liquity/LiquityTroveDeployment.s.sol b/src/deployment/liquity/LiquityTroveDeployment.s.sol index 79e00bac0..12a122b05 100644 --- a/src/deployment/liquity/LiquityTroveDeployment.s.sol +++ b/src/deployment/liquity/LiquityTroveDeployment.s.sol @@ -30,7 +30,7 @@ contract LiquityTroveDeployment is BaseDeployment { function deployAndList(uint256 _initialCr) public { address bridge = deploy(_initialCr); - uint256 addressId = listBridge(bridge, 550_000); + uint256 addressId = listBridge(bridge, 700_000); emit log_named_uint("Trove bridge address id", addressId); listAsset(TroveBridge(payable(bridge)).LUSD(), 55_000); diff --git a/src/gas/angle/AngleSLPGas.s.sol b/src/gas/angle/AngleSLPGas.s.sol index d2ee261ec..042e33964 100644 --- a/src/gas/angle/AngleSLPGas.s.sol +++ b/src/gas/angle/AngleSLPGas.s.sol @@ -28,7 +28,7 @@ contract AngleMeasure is AngleSLPDeployment { AztecTypes.AztecAsset internal wethAsset; AztecTypes.AztecAsset internal sanWethAsset; - function setUp() public override (BaseDeployment) { + function setUp() public override(BaseDeployment) { super.setUp(); address defiProxy = IRead(ROLLUP_PROCESSOR).defiBridgeProxy(); diff --git a/src/gas/erc4626/ERC4626Gas.s.sol b/src/gas/erc4626/ERC4626Gas.s.sol index 941c4ea80..e41a3f33b 100644 --- a/src/gas/erc4626/ERC4626Gas.s.sol +++ b/src/gas/erc4626/ERC4626Gas.s.sol @@ -36,7 +36,7 @@ contract ERC4626Measure is ERC4626Deployment { AztecTypes.AztecAsset internal daiAsset; AztecTypes.AztecAsset internal wcDaiAsset; // ERC4626-Wrapped Compound Dai - function setUp() public override (BaseDeployment) { + function setUp() public override(BaseDeployment) { super.setUp(); address defiProxy = IRead(ROLLUP_PROCESSOR).defiBridgeProxy(); diff --git a/src/gas/liquity/TroveBridgeGas.s.sol b/src/gas/liquity/TroveBridgeGas.s.sol index dc87c53cb..4b5421ab8 100644 --- a/src/gas/liquity/TroveBridgeGas.s.sol +++ b/src/gas/liquity/TroveBridgeGas.s.sol @@ -26,7 +26,7 @@ contract TroveBridgeMeasure is LiquityTroveDeployment { AztecTypes.AztecAsset internal lusdAsset; AztecTypes.AztecAsset internal tbAsset; // Accounting token - function setUp() public override (BaseDeployment) { + function setUp() public override(BaseDeployment) { super.setUp(); address defiProxy = IRead(ROLLUP_PROCESSOR).defiBridgeProxy(); diff --git a/src/gas/uniswap/UniswapGas.s.sol b/src/gas/uniswap/UniswapGas.s.sol index 8637dc933..33975c8a3 100644 --- a/src/gas/uniswap/UniswapGas.s.sol +++ b/src/gas/uniswap/UniswapGas.s.sol @@ -33,7 +33,7 @@ contract UniswapMeasure is UniswapDeployment { UniswapBridge.SplitPath internal emptySplitPath; - function setUp() public override (BaseDeployment) { + function setUp() public override(BaseDeployment) { super.setUp(); address defiProxy = IRead(ROLLUP_PROCESSOR).defiBridgeProxy(); diff --git a/src/test/bridges/erc4626/mocks/WETHVault.sol b/src/test/bridges/erc4626/mocks/WETHVault.sol index cb5e4d484..d75ea7399 100644 --- a/src/test/bridges/erc4626/mocks/WETHVault.sol +++ b/src/test/bridges/erc4626/mocks/WETHVault.sol @@ -10,7 +10,7 @@ contract WETHVault is ERC20("WETH vault", "vWETH"), ERC4626(IERC20Metadata(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2)) { - function decimals() public pure override (ERC20, ERC4626) returns (uint8) { + function decimals() public pure override(ERC20, ERC4626) returns (uint8) { return 18; } } diff --git a/src/test/bridges/liquity/TroveBridgeE2E.t.sol b/src/test/bridges/liquity/TroveBridgeE2E.t.sol index a4092901c..3f8b033a5 100644 --- a/src/test/bridges/liquity/TroveBridgeE2E.t.sol +++ b/src/test/bridges/liquity/TroveBridgeE2E.t.sol @@ -34,11 +34,11 @@ contract TroveBridgeE2ETest is BridgeTestBase, TroveBridgeTestBase { vm.startPrank(MULTI_SIG); // List trove bridge with a gasLimit of 500k - ROLLUP_PROCESSOR.setSupportedBridge(address(bridge), 600_000); + ROLLUP_PROCESSOR.setSupportedBridge(address(bridge), 700_000); // List the assets with a gasLimit of 100k - ROLLUP_PROCESSOR.setSupportedAsset(tokens["LUSD"].addr, 100000); - ROLLUP_PROCESSOR.setSupportedAsset(address(bridge), 100000); + ROLLUP_PROCESSOR.setSupportedAsset(tokens["LUSD"].addr, 55_000); + ROLLUP_PROCESSOR.setSupportedAsset(address(bridge), 55_000); vm.stopPrank(); diff --git a/src/test/bridges/liquity/utils/MockPriceFeed.sol b/src/test/bridges/liquity/utils/MockPriceFeed.sol index b1ea7c1a8..5df5f19b0 100644 --- a/src/test/bridges/liquity/utils/MockPriceFeed.sol +++ b/src/test/bridges/liquity/utils/MockPriceFeed.sol @@ -12,7 +12,7 @@ contract MockPriceFeed is IPriceFeed { lastGoodPrice = _price; } - function fetchPrice() external override (IPriceFeed) returns (uint256) { + function fetchPrice() external override(IPriceFeed) returns (uint256) { return lastGoodPrice; } } From f90f80abf1a5c67c0bf76c964383c7ea660280df Mon Sep 17 00:00:00 2001 From: Lasse Herskind <16536249+LHerskind@users.noreply.github.com> Date: Mon, 9 Jan 2023 16:48:48 +0100 Subject: [PATCH 12/24] Lh/deployments update (#294) * feat: add element deployment + update workaround * fix: remapping + clarity improvement data provider deployment * fix: formatting * fix: Add missing broadcast in element deployment s * fix: we be formatting Co-authored-by: cheethas --- foundry.toml | 2 +- src/deployment/AggregateDeployment.s.sol | 30 +++++++++++- src/deployment/base/BaseDeployment.s.sol | 20 ++++---- .../dataprovider/DataProviderDeployment.s.sol | 17 ++----- .../element/ElementDeployment.s.sol | 47 +++++++++++++++++++ 5 files changed, 88 insertions(+), 28 deletions(-) create mode 100644 src/deployment/element/ElementDeployment.s.sol diff --git a/foundry.toml b/foundry.toml index 4e0dedb99..08f69bbd7 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,7 +2,7 @@ src = 'src' out = 'out' libs = ['lib'] -remappings = ['@openzeppelin/=lib/openzeppelin-contracts/', 'forge-std/=lib/forge-std/src', 'rollup-encoder/=lib/rollup-encoder/src'] +remappings = ['@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/', 'forge-std/=lib/forge-std/src', 'rollup-encoder/=lib/rollup-encoder/src'] fuzz_runs = 256 gas_reports = ["*"] eth-rpc-url = 'https://mainnet.infura.io/v3/9928b52099854248b3a096be07a6b23c' diff --git a/src/deployment/AggregateDeployment.s.sol b/src/deployment/AggregateDeployment.s.sol index a4008cfa4..e981d257e 100644 --- a/src/deployment/AggregateDeployment.s.sol +++ b/src/deployment/AggregateDeployment.s.sol @@ -9,6 +9,7 @@ import {BaseDeployment} from "./base/BaseDeployment.s.sol"; import {IRollupProcessor} from "rollup-encoder/interfaces/IRollupProcessor.sol"; import {DataProvider} from "../aztec/DataProvider.sol"; +import {ElementDeployment} from "./element/ElementDeployment.s.sol"; import {CurveDeployment} from "./curve/CurveDeployment.s.sol"; import {DonationDeployment} from "./donation/DonationDeployment.s.sol"; import {ERC4626Deployment} from "./erc4626/ERC4626Deployment.s.sol"; @@ -27,11 +28,36 @@ import {CurveStethLpDeployment} from "./curve/CurveStethLpDeployment.s.sol"; contract AggregateDeployment is BaseDeployment { address internal erc4626Bridge; - function deployAndListAll() public { + function deployAndListAll() public returns (address) { DataProviderDeployment dataProviderDeploy = new DataProviderDeployment(); dataProviderDeploy.setUp(); address dataProvider = dataProviderDeploy.deploy(); + emit log("--- Element ---"); + { + ElementDeployment elementDeployment = new ElementDeployment(); + elementDeployment.setUp(); + address elementBridge = elementDeployment.deployAndList(); + + address wrappedDai = 0x3A285cbE492cB172b78E76Cf4f15cb6Fe9f162E4; + + elementDeployment.registerPool( + elementBridge, 0x71628c66C502F988Fbb9e17081F2bD14e361FAF4, wrappedDai, 1634346845 + ); + elementDeployment.registerPool( + elementBridge, 0xA47D1251CF21AD42685Cc6B8B3a186a73Dbd06cf, wrappedDai, 1643382446 + ); + elementDeployment.registerPool( + elementBridge, 0xEdf085f65b4F6c155e13155502Ef925c9a756003, wrappedDai, 1651275535 + ); + elementDeployment.registerPool( + elementBridge, 0x8fFD1dc7C3eF65f833CF84dbCd15b6Ad7f9C54EC, wrappedDai, 1663361092 + ); + elementDeployment.registerPool( + elementBridge, 0x7F4A33deE068C4fA012d64677C61519a578dfA35, wrappedDai, 1677243924 + ); + } + emit log("--- Curve ---"); { CurveDeployment curveDeployment = new CurveDeployment(); @@ -162,6 +188,8 @@ contract AggregateDeployment is BaseDeployment { { dataProviderDeploy.updateNames(dataProvider); } + + return dataProvider; } function readStats() public { diff --git a/src/deployment/base/BaseDeployment.s.sol b/src/deployment/base/BaseDeployment.s.sol index 1fc5e4ce7..c8d70e8f9 100644 --- a/src/deployment/base/BaseDeployment.s.sol +++ b/src/deployment/base/BaseDeployment.s.sol @@ -41,14 +41,6 @@ abstract contract BaseDeployment is Test { address internal ROLLUP_PROCESSOR; address internal LISTER; - function setRollupProcessor(address _rollupProcessor) public { - ROLLUP_PROCESSOR = _rollupProcessor; - } - - function setLister(address _lister) public { - LISTER = _lister; - } - /* solhint-enable var-name-mixedcase */ function setUp() public virtual { @@ -67,10 +59,16 @@ abstract contract BaseDeployment is Test { } else { NETWORK = Network.DONT_CARE; MODE = Mode.BROADCAST; + /* solhint-disable var-name-mixedcase */ + string memory rollup_processor_key = "ROLLUP_PROCESSOR_ADDRESS"; + string memory lister_key = "LISTER_ADDRESS"; + /* solhint-enable var-name-mixedcase */ + + ROLLUP_PROCESSOR = vm.envAddress(rollup_processor_key); + LISTER = vm.envAddress(lister_key); + require(ROLLUP_PROCESSOR != address(0), "RollupProcessor address resolved to 0"); require(LISTER != address(0), "Lister address resolved to 0"); - emit log_named_address("Rollup at", ROLLUP_PROCESSOR); - emit log_named_address("Lister at", LISTER); return; } @@ -88,8 +86,6 @@ abstract contract BaseDeployment is Test { /* solhint-disable custom-error-over-require */ require(ROLLUP_PROCESSOR != address(0), "RollupProcessor address resolved to 0"); require(LISTER != address(0), "Lister address resolved to 0"); - emit log_named_address("Rollup at", ROLLUP_PROCESSOR); - emit log_named_address("Lister at", LISTER); } /** diff --git a/src/deployment/dataprovider/DataProviderDeployment.s.sol b/src/deployment/dataprovider/DataProviderDeployment.s.sol index 98da098cb..ce56cf0be 100644 --- a/src/deployment/dataprovider/DataProviderDeployment.s.sol +++ b/src/deployment/dataprovider/DataProviderDeployment.s.sol @@ -111,20 +111,9 @@ contract DataProviderDeployment is BaseDeployment { uint256[] memory bridgeAddressIds = new uint256[](13); string[] memory bridgeTags = new string[](13); - - bridgeAddressIds[0] = 1; - bridgeAddressIds[1] = 5; - bridgeAddressIds[2] = 6; - bridgeAddressIds[3] = 7; - bridgeAddressIds[4] = 8; - bridgeAddressIds[5] = 9; - bridgeAddressIds[6] = 10; - bridgeAddressIds[7] = 11; - bridgeAddressIds[8] = 12; - bridgeAddressIds[9] = 13; - bridgeAddressIds[10] = 14; - bridgeAddressIds[11] = 15; - bridgeAddressIds[12] = 16; + for (uint256 i = 0; i < bridgeAddressIds.length; i++) { + bridgeAddressIds[i] = i + 1; + } bridgeTags[0] = "ElementBridge_800K"; bridgeTags[1] = "CurveStEthBridge_250K"; diff --git a/src/deployment/element/ElementDeployment.s.sol b/src/deployment/element/ElementDeployment.s.sol new file mode 100644 index 000000000..6fbf934db --- /dev/null +++ b/src/deployment/element/ElementDeployment.s.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec. +pragma solidity >=0.8.4; + +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +import {BaseDeployment} from "../base/BaseDeployment.s.sol"; +import {ElementBridge} from "../../bridges/element/ElementBridge.sol"; + +contract ElementDeployment is BaseDeployment { + address internal constant TRANCHE_FACTORY = 0x62F161BF3692E4015BefB05A03a94A40f520d1c0; + address internal constant ELEMENT_REGISTRY_ADDRESS = 0xc68e2BAb13a7A2344bb81badBeA626012C62C510; + bytes32 internal constant TRANCHE_BYTECODE_HASH = 0xf481a073666136ab1f5e93b296e84df58092065256d0db23b2d22b62c68e978d; + address internal constant BALANCER_VAULT = 0xBA12222222228d8Ba445958a75a0704d566BF2C8; + + function deployAndList() public returns (address) { + emit log("Deploying element bridge"); + vm.broadcast(); + ElementBridge bridge = new ElementBridge( + ROLLUP_PROCESSOR, + TRANCHE_FACTORY, + TRANCHE_BYTECODE_HASH, + BALANCER_VAULT, + ELEMENT_REGISTRY_ADDRESS + ); + emit log_named_address("element bridge deployed to", address(bridge)); + + uint256 addressId = listBridge(address(bridge), 800000); + emit log_named_uint("Curve bridge address id", addressId); + + return address(bridge); + } + + function registerPool(address _bridge, address _pool, address _position, uint64 _expiry) public { + if (_expiry < block.timestamp) { + return; + } + + string memory symbol = IERC20Metadata(_position).symbol(); + string memory s = string(abi.encodePacked("Registering ", symbol, " pool with expiry at ")); + + emit log_named_uint(s, _expiry); + + vm.broadcast(); + ElementBridge(_bridge).registerConvergentPoolAddress(_pool, _position, _expiry); + } +} From 590067749c56ff5dfc12f1de090b22f1c3aa06b2 Mon Sep 17 00:00:00 2001 From: Maddiaa <47148561+cheethas@users.noreply.github.com> Date: Mon, 9 Jan 2023 16:08:27 +0000 Subject: [PATCH 13/24] nits: capitalize env vars (#300) --- README.md | 8 ++++---- specs/aztec/dataprovider/readme.md | 6 +++--- src/deployment/base/BaseDeployment.s.sol | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 09fc64f39..08af5b5af 100644 --- a/README.md +++ b/README.md @@ -49,8 +49,8 @@ To get started follow the steps below: 6. Write a deployment script. Make a script that inherits from the `BaseDeployment.s.sol` file. The base provides helper functions for listing assets/bridges and a getter for the rollup address. - Use the env variables `simulateAdmin=false|true` and `network=mainnet|devnet|testnet` to specify how to run it. - With `simulateAdmin=true`, the `listBridge` and `listAsset` helpers will be impersonating an account with access to list, otherwise they are broadcasted. See the example scripts from other bridges for inspiration on how to write the scripts. + Use the env variables `SIMULATE_ADMIN=false|true` and `NETWORK=mainnet|devnet|testnet` to specify how to run it. + With `SIMULATE_ADMIN=true`, the `listBridge` and `listAsset` helpers will be impersonating an account with access to list, otherwise they are broadcasted. See the example scripts from other bridges for inspiration on how to write the scripts. 7. Testing/using your deployment script To run your deployment script, you need to set the environment up first, this include specifying the RPC you will be sending the transactions to and the environment variables from above. You can do it using a private key, or even with a Ledger or Trezor, see the foundry book for more info on using hardware wallets. @@ -62,8 +62,8 @@ To get started follow the steps below: # on testnet this will fetch from the Aztec endpoints, on mainnet, this will lookup the `rollup.aztec.eth` ens export RPC=https://aztec-connect-testnet-eth-host.aztec.network:8545 export PRIV_KEY= # If using a private-key directly - export network=testnet # When using the testnet - export simulateAdmin=false # When you want to broadcast, use `true` if simulating admin + export NETWORK=testnet # When using the testnet + export SIMULATE_ADMIN=false # When you want to broadcast, use `true` if simulating admin forge script --fork-url $RPC --ffi --legacy --private-key $PRIV --sig "" --broadcast # Example script reading the current assets and bridges: diff --git a/specs/aztec/dataprovider/readme.md b/specs/aztec/dataprovider/readme.md index 2011a1701..78c2e127e 100644 --- a/specs/aztec/dataprovider/readme.md +++ b/specs/aztec/dataprovider/readme.md @@ -19,17 +19,17 @@ function getAssets() public view returns (AssetData[] memory); function getBridges() public view returns (BridgeData[] memory); ``` -The easiest way to access it, if already using this repository is to execute the `read()` script in the DataProviderDeployment solidity file. It holds addresses for mainnet, testnet and devnet. +The easiest way to access it, if already using this repository is to execute the `read()` script in the DataProviderDeployment solidity file. It holds addresses for mainnet, testnet and devnet. ```bash -export network= && export simulateAdmin=false +export NETWORK= && export SIMULATE_ADMIN=false export ETH_RPC_URL= forge script DataProviderDeployment --rpc-url $ETH_RPC_URL --sig "read()" ``` ## Usage by owner -Updating values stored in the data provider is only possible by the owner of the contract. +Updating values stored in the data provider is only possible by the owner of the contract. Before running the commands bellow export relevant environment variables: diff --git a/src/deployment/base/BaseDeployment.s.sol b/src/deployment/base/BaseDeployment.s.sol index c8d70e8f9..e59fee53a 100644 --- a/src/deployment/base/BaseDeployment.s.sol +++ b/src/deployment/base/BaseDeployment.s.sol @@ -45,7 +45,7 @@ abstract contract BaseDeployment is Test { function setUp() public virtual { // Read from the .env - string memory networkKey = "network"; + string memory networkKey = "NETWORK"; string memory envNetwork = vm.envString(networkKey); bytes32 envNetworkHash = keccak256(abi.encodePacked(envNetwork)); @@ -72,7 +72,7 @@ abstract contract BaseDeployment is Test { return; } - string memory modeKey = "simulateAdmin"; + string memory modeKey = "SIMULATE_ADMIN"; bool envMode = vm.envBool(modeKey); MODE = envMode ? Mode.SIMULATE_ADMIN : Mode.BROADCAST; From c7df8f836391890843bd2bb6c03ed77dc85530b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Bene=C5=A1?= Date: Mon, 9 Jan 2023 10:37:26 -0600 Subject: [PATCH 14/24] Subsidy log script (#293) * feat: listing erc4626 subsidies * feat: finished listing erc4626 subsidies * feat: yearn subsidy listing + list all flag * feat: ERC4626 tags * feat: displaying cost estimation for a month * refactor: improved naming * feat: getting info from env * fix: use log instead of list * fix: spelling * fix: use decimals for easier read * fix: print constants used Co-authored-by: LHerskind --- src/scripts/SubsidyLogger.sol | 149 ++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 src/scripts/SubsidyLogger.sol diff --git a/src/scripts/SubsidyLogger.sol b/src/scripts/SubsidyLogger.sol new file mode 100644 index 000000000..630c4ff50 --- /dev/null +++ b/src/scripts/SubsidyLogger.sol @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec. +pragma solidity >=0.8.4; + +import {Test} from "forge-std/Test.sol"; +import {BridgeBase} from "../bridges/base/BridgeBase.sol"; +import {AztecTypes} from "rollup-encoder/libraries/AztecTypes.sol"; +import {ISubsidy} from "../aztec/interfaces/ISubsidy.sol"; +import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; + +/** + * @title Script which logs either all subsidized or non-subsidized bridges + * @author Aztec team + * @dev execute with: ONLY_EMPTY=true && forge script src/scripts/SubsidyLogger.sol:SubsidyLogger --fork-url $RPC --sig "logSubsidies()" + */ +contract SubsidyLogger is Test { + ISubsidy public constant SUBSIDY = ISubsidy(0xABc30E831B5Cc173A9Ed5941714A7845c909e7fA); + // @dev A time period denominated in hours indicating after what time a call is fully subsidized + uint256 public constant FULL_SUBSIDY_TIME = 36; + uint256 public constant ESTIMATION_BASE_FEE = 2e10; // 20 gwei + + address[] private erc4626Shares = [ + 0x3c66B18F67CA6C1A71F829E2F6a0c987f97462d0, // ERC4626-Wrapped Euler WETH (weWETH) + 0x4169Df1B7820702f566cc10938DA51F6F597d264, // ERC4626-Wrapped Euler DAI (weDAI) + 0x60897720AA966452e8706e74296B018990aEc527, // ERC4626-Wrapped Euler wstETH (wewstETH) + 0xbcb91e0B4Ad56b0d41e0C168E3090361c0039abC, // ERC4626-Wrapped AAVE V2 DAI (wa2DAI) + 0xc21F107933612eCF5677894d45fc060767479A9b // ERC4626-Wrapped AAVE V2 WETH (wa2WETH) + ]; + + string[] private erc4626Tags = [ + "ERC4626-Wrapped Euler WETH (weWETH)", + "ERC4626-Wrapped Euler DAI (weDAI)", + "ERC4626-Wrapped Euler wstETH (wewstETH)", + "ERC4626-Wrapped AAVE V2 DAI (wa2DAI)", + "ERC4626-Wrapped AAVE V2 WETH (wa2WETH)" + ]; + + AztecTypes.AztecAsset internal emptyAsset; + // @dev if set to true only subsidies which need to be funded get displayed + bool public onlyEmpty = false; + + function setUp() public { + onlyEmpty = vm.envBool("ONLY_EMPTY"); + emit log_named_uint("Hours for full subsidy", FULL_SUBSIDY_TIME); + emit log_named_decimal_uint("Gas price used in gwei", ESTIMATION_BASE_FEE, 9); + } + + function logSubsidies() public { + logERC4626Subsidies(); + logYearnSubsidies(); + } + + function logERC4626Subsidies() public { + BridgeBase bridge = BridgeBase(0x3578D6D5e1B4F07A48bb1c958CBfEc135bef7d98); + + emit log_string("\n"); + emit log_string("=========== ERC4626 Bridge ============="); + emit log_named_address("Bridge address", address(bridge)); + + for (uint256 i = 0; i < erc4626Shares.length; i++) { + address share = erc4626Shares[i]; + address asset = IERC4626(share).asset(); + + AztecTypes.AztecAsset memory shareAsset = AztecTypes.AztecAsset({ + id: 0, // ID is not used when computing criteria and for this reason can be incorrect + erc20Address: share, + assetType: AztecTypes.AztecAssetType.ERC20 + }); + AztecTypes.AztecAsset memory assetAsset = AztecTypes.AztecAsset({ + id: 0, // ID is not used when computing criteria and for this reason can be incorrect + erc20Address: asset, + assetType: AztecTypes.AztecAssetType.ERC20 + }); + + uint256 enterCriteria = bridge.computeCriteria(assetAsset, emptyAsset, shareAsset, emptyAsset, 0); + uint256 exitCriteria = bridge.computeCriteria(shareAsset, emptyAsset, assetAsset, emptyAsset, 0); + + ISubsidy.Subsidy memory enterSubsidy = SUBSIDY.getSubsidy(address(bridge), enterCriteria); + ISubsidy.Subsidy memory exitSubsidy = SUBSIDY.getSubsidy(address(bridge), exitCriteria); + + if (enterSubsidy.available == 0 || !onlyEmpty) { + uint256 gasPerMinute = enterSubsidy.gasUsage / (FULL_SUBSIDY_TIME * 60); + emit log_string("========================"); + emit log_string(erc4626Tags[i]); + emit log_named_address("share", share); + emit log_named_uint("enterCriteria", enterCriteria); + emit log_named_uint("enterCriteria available", enterSubsidy.available); + emit log_named_uint("enterCriteria gasUsage", enterSubsidy.gasUsage); + emit log_named_uint("recommended minGasPerMinute", gasPerMinute); + uint256 costOfMonth = enterSubsidy.gasUsage * (24 * 30) * ESTIMATION_BASE_FEE / FULL_SUBSIDY_TIME; + emit log_named_decimal_uint("cost of fully subsidizing for a month", costOfMonth, 18); + } + + if (exitSubsidy.available == 0 || !onlyEmpty) { + uint256 gasPerMinute = exitSubsidy.gasUsage / (FULL_SUBSIDY_TIME * 60); + emit log_string("========================"); + emit log_string(erc4626Tags[i]); + emit log_named_address("share", share); + emit log_named_uint("exitCriteria", exitCriteria); + emit log_named_uint("exitCriteria available", exitSubsidy.available); + emit log_named_uint("exitCriteria gasUsage", exitSubsidy.gasUsage); + emit log_named_uint("recommended minGasPerMinute", gasPerMinute); + uint256 costOfMonth = exitSubsidy.gasUsage * (24 * 30) * ESTIMATION_BASE_FEE / FULL_SUBSIDY_TIME; + emit log_named_decimal_uint("cost of fully subsidizing for a month", costOfMonth, 18); + } + } + + emit log_string("========================"); + } + + function logYearnSubsidies() public { + // bridge address asset combination gas usage subsidy + BridgeBase bridge = BridgeBase(0xE71A50a78CcCff7e20D8349EED295F12f0C8C9eF); + + emit log_string("\n"); + emit log_string("=========== Yearn Bridge ============="); + emit log_named_address("Bridge address", address(bridge)); + + uint256 enterCriteria = 0; + uint256 exitCriteria = 1; + + ISubsidy.Subsidy memory enterSubsidy = SUBSIDY.getSubsidy(address(bridge), enterCriteria); + ISubsidy.Subsidy memory exitSubsidy = SUBSIDY.getSubsidy(address(bridge), exitCriteria); + + if (enterSubsidy.available == 0 || !onlyEmpty) { + uint256 gasPerMinute = enterSubsidy.gasUsage / (FULL_SUBSIDY_TIME * 60); + emit log_string("========================"); + emit log_named_uint("enterCriteria", enterCriteria); + emit log_named_uint("enterCriteria available", enterSubsidy.available); + emit log_named_uint("enterCriteria gasUsage", enterSubsidy.gasUsage); + emit log_named_uint("recommended minGasPerMinute", gasPerMinute); + uint256 costOfMonth = enterSubsidy.gasUsage * (24 * 30) * ESTIMATION_BASE_FEE / FULL_SUBSIDY_TIME; + emit log_named_decimal_uint("cost of fully subsidizing for a month", costOfMonth, 18); + } + + if (exitSubsidy.available == 0 || !onlyEmpty) { + uint256 gasPerMinute = exitSubsidy.gasUsage / (FULL_SUBSIDY_TIME * 60); + emit log_string("========================"); + emit log_named_uint("exitCriteria", exitCriteria); + emit log_named_uint("exitCriteria available", exitSubsidy.available); + emit log_named_uint("exitCriteria gasUsage", exitSubsidy.gasUsage); + emit log_named_uint("recommended minGasPerMinute", gasPerMinute); + uint256 costOfMonth = exitSubsidy.gasUsage * (24 * 30) * ESTIMATION_BASE_FEE / FULL_SUBSIDY_TIME; + emit log_named_decimal_uint("cost of fully subsidizing for a month", costOfMonth, 18); + } + + emit log_string("========================"); + } +} From a25f80c464c37b9ef8dc8136bd11c016c77fbac0 Mon Sep 17 00:00:00 2001 From: LHerskind Date: Tue, 10 Jan 2023 11:49:20 +0000 Subject: [PATCH 15/24] fix: handle case on devnet where basefee is 0 --- src/aztec/DataProvider.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/aztec/DataProvider.sol b/src/aztec/DataProvider.sol index 8e0c079ff..262a08655 100644 --- a/src/aztec/DataProvider.sol +++ b/src/aztec/DataProvider.sol @@ -226,7 +226,7 @@ contract DataProvider is Ownable { uint256 criteria = abi.decode(returnData, (uint256)); uint256 ethSub = SUBSIDY.getAccumulatedSubsidyAmount(vars.bridgeAddress, criteria); - return (criteria, ethSub, ethSub / block.basefee); + return (criteria, ethSub, block.basefee == 0 ? 0 : ethSub / block.basefee); } function _aztecAsset(uint256 _assetId) internal view returns (AztecTypes.AztecAsset memory) { From 56ab87eae2f02bb8475431f720941e297441a409 Mon Sep 17 00:00:00 2001 From: LHerskind Date: Tue, 10 Jan 2023 11:59:37 +0000 Subject: [PATCH 16/24] fix: cleanup + add a test --- src/aztec/DataProvider.sol | 3 ++- src/test/aztec/dataprovider/DataProvider.t.sol | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/aztec/DataProvider.sol b/src/aztec/DataProvider.sol index 262a08655..2e96e6012 100644 --- a/src/aztec/DataProvider.sol +++ b/src/aztec/DataProvider.sol @@ -225,8 +225,9 @@ contract DataProvider is Ownable { uint256 criteria = abi.decode(returnData, (uint256)); uint256 ethSub = SUBSIDY.getAccumulatedSubsidyAmount(vars.bridgeAddress, criteria); + uint256 gasCovered = block.basefee == 0 ? 0 : ethSub / block.basefee; - return (criteria, ethSub, block.basefee == 0 ? 0 : ethSub / block.basefee); + return (criteria, ethSub, gasCovered); } function _aztecAsset(uint256 _assetId) internal view returns (AztecTypes.AztecAsset memory) { diff --git a/src/test/aztec/dataprovider/DataProvider.t.sol b/src/test/aztec/dataprovider/DataProvider.t.sol index 07ddc8464..6d3407dea 100644 --- a/src/test/aztec/dataprovider/DataProvider.t.sol +++ b/src/test/aztec/dataprovider/DataProvider.t.sol @@ -238,4 +238,20 @@ contract DataProviderTest is BridgeTestBase { assertGe(subsidy, gasUnits * block.basefee, "Invalid gas units accrued"); assertEq(gasUnits, subsidy / block.basefee, "Invalid gas units accrued"); } + + function testZeroBaseFee() public { + AztecTypes.AztecAsset memory eth = ROLLUP_ENCODER.getRealAztecAsset(address(0)); + AztecTypes.AztecAsset memory virtualAsset = + AztecTypes.AztecAsset({id: 0, erc20Address: address(0), assetType: AztecTypes.AztecAssetType.VIRTUAL}); + + uint256 bridgeCallData = ROLLUP_ENCODER.encodeBridgeCallData(7, virtualAsset, eth, virtualAsset, eth, 0); + + vm.fee(0); + + (uint256 criteria, uint256 subsidy, uint256 gasUnits) = provider.getAccumulatedSubsidyAmount(bridgeCallData); + assertEq(criteria, 0, "Wrong criteria"); + assertEq(subsidy, 0, "No subsidy accrued"); + assertEq(subsidy, 0, "Invalid gas units accrued"); + assertEq(gasUnits, 0, "Invalid gas units accrued"); + } } From 24d672a779d60b15c0ebc62a3c683aca590413a1 Mon Sep 17 00:00:00 2001 From: Lasse Herskind <16536249+LHerskind@users.noreply.github.com> Date: Wed, 18 Jan 2023 15:29:24 +0000 Subject: [PATCH 17/24] fix: pin BiDCA tests to block 14950000 (#310) --- package.json | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 84a21b2be..42973542c 100644 --- a/package.json +++ b/package.json @@ -14,17 +14,16 @@ "build": "forge build", "compile:typechain": "yarn clean && forge build --skip test --skip script && typechain --target ethers-v5 --out-dir ./typechain-types './out/?(DataProvider|RollupProcessor|*Bridge|I*).sol/*.json'", "test:pinned:14000000": "forge test --fork-block-number 14000000 --match-contract 'Element' --fork-url https://mainnet.infura.io/v3/9928b52099854248b3a096be07a6b23c", + "test:pinned:14950000": "forge test --fork-block-number 14950000 --match-contract 'BiDCA' --fork-url https://mainnet.infura.io/v3/9928b52099854248b3a096be07a6b23c", "test:pinned:14970000": "forge test --fork-block-number 14970000 -m 'testRedistributionSuccessfulSwap|testRedistributionExitWhenICREqualsMCR' --fork-url https://mainnet.infura.io/v3/9928b52099854248b3a096be07a6b23c", "test:pinned:14972000": "forge test --fork-block-number 14972000 -m 'testRedistributionFailingSwap' --fork-url https://mainnet.infura.io/v3/9928b52099854248b3a096be07a6b23c", - "test:pinned": "yarn test:pinned:14000000 && yarn test:pinned:14970000 && yarn test:pinned:14972000", - "test": "forge test --no-match-contract 'Element' --no-match-test 'testRedistribution' && yarn test:pinned", + "test:pinned": "yarn test:pinned:14000000 && yarn test:pinned:14950000 && yarn test:pinned:14970000 && yarn test:pinned:14972000", + "test": "forge test --no-match-contract 'Element|BiDCA' --no-match-test 'testRedistribution' && yarn test:pinned", "formatting": "forge fmt", "formatting:check": "forge fmt --check", "lint": "solhint --config ./.solhint.json --fix \"src/**/*.sol\"" }, "devDependencies": { - "@typechain/ethers-v5": "^10.1.1", - "ethers": "^5.7.2", "solhint": "https://github.com/LHerskind/solhint", "typechain": "^8.1.1", "typescript": "^4.9.3" From e7f4b853d960207e1f9702932d698f4a6924cee8 Mon Sep 17 00:00:00 2001 From: Lasse Herskind <16536249+LHerskind@users.noreply.github.com> Date: Thu, 19 Jan 2023 14:07:07 +0000 Subject: [PATCH 18/24] chore: removed `read()` and updates readme. (#312) --- specs/aztec/dataprovider/readme.md | 4 ++-- .../dataprovider/DataProviderDeployment.s.sol | 10 ---------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/specs/aztec/dataprovider/readme.md b/specs/aztec/dataprovider/readme.md index 78c2e127e..7c67b34ce 100644 --- a/specs/aztec/dataprovider/readme.md +++ b/specs/aztec/dataprovider/readme.md @@ -19,12 +19,12 @@ function getAssets() public view returns (AssetData[] memory); function getBridges() public view returns (BridgeData[] memory); ``` -The easiest way to access it, if already using this repository is to execute the `read()` script in the DataProviderDeployment solidity file. It holds addresses for mainnet, testnet and devnet. +The easiest way to access it, if already using this repository is to execute the `readProvider(address _provider)` script in the DataProviderDeployment solidity file. To get the address of the data provider, you can query the falafel status endpoint `https://api.aztec.network//falafel/status` with `DEPLOY_TAG=`. ```bash export NETWORK= && export SIMULATE_ADMIN=false export ETH_RPC_URL= -forge script DataProviderDeployment --rpc-url $ETH_RPC_URL --sig "read()" +forge script DataProviderDeployment --rpc-url $ETH_RPC_URL --sig "readProvider(address)" DATA_PROVIDER_ADDRESS ``` ## Usage by owner diff --git a/src/deployment/dataprovider/DataProviderDeployment.s.sol b/src/deployment/dataprovider/DataProviderDeployment.s.sol index ce56cf0be..7dd517150 100644 --- a/src/deployment/dataprovider/DataProviderDeployment.s.sol +++ b/src/deployment/dataprovider/DataProviderDeployment.s.sol @@ -20,16 +20,6 @@ contract DataProviderDeployment is BaseDeployment { return address(provider); } - function read() public { - address provider = 0x8B2E54fa4398C8f7502f30aC94Cb1f354390c8ab; - if (block.chainid == 3567) { - provider = 0xD25B8B044CE58eaBF41288E223609726A6c98e44; - } else if (block.chainid == 0xa57ec) { - provider = 0xA33B20Ba45cA9C265bbF7b35a75717590EDfc868; - } - readProvider(provider); - } - function readProvider(address _provider) public { DataProvider provider = DataProvider(_provider); From 8a143dd0e75a9efd887478c61e0d12af3b28ac7f Mon Sep 17 00:00:00 2001 From: Cash Baller Date: Tue, 24 Jan 2023 06:06:15 -0600 Subject: [PATCH 19/24] Fixed small typos in the ElementBridge contract (#314) --- src/bridges/element/ElementBridge.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/bridges/element/ElementBridge.sol b/src/bridges/element/ElementBridge.sol index 2a7b5603b..09b304457 100644 --- a/src/bridges/element/ElementBridge.sol +++ b/src/bridges/element/ElementBridge.sol @@ -101,7 +101,7 @@ contract ElementBridge is BridgeBase { * @param quantityTokensHeld total quantity of principal tokens purchased for the tranche * @param quantityAssetRedeemed total quantity of underlying tokens received from the element tranche on expiry * @param quantityAssetRemaining the current remainning quantity of underlying tokens held by the contract - * @param numDeposits the total number of deposits (interactions) against the give tranche + * @param numDeposits the total number of deposits (interactions) against the given tranche * @param numFinalised the current number of interactions against this tranche that have been finalised * @param redemptionStatus value describing the redemption status of the tranche */ @@ -129,7 +129,7 @@ contract ElementBridge is BridgeBase { // cache of all pools we have been configured to interact with mapping(uint256 => Pool) public pools; - // cahce of all of our tranche accounts + // cache of all of our tranche accounts mapping(address => TrancheAccount) private trancheAccounts; // mapping containing the block number in which a tranche was configured @@ -145,7 +145,7 @@ contract ElementBridge is BridgeBase { MinHeap.MinHeapData private heap; mapping(uint64 => uint256[]) private expiryToNonce; - // 48 hours in seconds, usd for calculating speeedbump expiries + // 48 hours in seconds, usd for calculating speedbump expiries uint256 internal constant FORTY_EIGHT_HOURS = 172800; uint256 internal constant MAX_UINT = type(uint256).max; @@ -167,7 +167,7 @@ contract ElementBridge is BridgeBase { /** * @dev Constructor * @param _rollupProcessor the address of the rollup contract - * @param _trancheFactory the address of the element tranche factor contract + * @param _trancheFactory the address of the element tranche factory contract * @param _trancheBytecodeHash the hash of the bytecode of the tranche contract, used for tranche contract address derivation * @param _balancerVaultAddress the address of the balancer router contract * @param _elementDeploymentValidatorAddress the address of the element deployment validator contract From bff4780cad7ac05be30ca3f7c28f72cfb0b8fe6c Mon Sep 17 00:00:00 2001 From: SanChuan <2194167956@qq.com> Date: Thu, 5 Jan 2023 12:26:02 +0000 Subject: [PATCH 20/24] add specs --- specs/bridges/nft_trading/readme.md | 46 +++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 specs/bridges/nft_trading/readme.md diff --git a/specs/bridges/nft_trading/readme.md b/specs/bridges/nft_trading/readme.md new file mode 100644 index 000000000..1e226df85 --- /dev/null +++ b/specs/bridges/nft_trading/readme.md @@ -0,0 +1,46 @@ +# Spec for NFT Trading Bridge + +## What does the bridge do? Why build it? + +This bridge enables user to trading their NFT on the established Layer 1 NFT marketplaces, aka [OpenSea](https://opensea.io/), where users can list and purchase NFTs from Aztec L2 without revealing their L1 identities. +In this way, the owner of one NFT owner is in private with the power of Aztec's zero knowledge rollup. So this bridge can enhance the secret characterist of Layer 1 user. + + +## What protocol(s) does the bridge interact with ? + + +The bridge interacts with [OpenSea](https://opensea.io/). + +## What is the flow of the bridge? +The simple flow as below: +1. User A on Aztec chain bridge interface pay and buy an NFT on Opensea in Ethereum. +2. The "Buy" order and relevant fee is send to L1 by Aztec's rollup. +3. The bridge contract in L1 is triggered to interact with OpenSea protocol. +4. Then the bridge contract own this NFT and record it to a private user id. +5. The User A On Aztec can redeem this NFT anytime, with his valid signature. + + + +### General Properties of convert(...) function + +- The bridge is synchronous, and will always return `isAsync = false`. + +- The bridge uses `_auxData` to encode the target NFT, id, price. + +- The Bridge perform token pre-approvals to allow the `ROLLUP_PROCESSOR` and `UNI_ROUTER` to pull tokens from it. + This is to reduce gas-overhead when performing the actions. It is safe to do, as the bridge is not holding the funds itself. + +## Is the contract upgradeable? + +No, the bridge is immutable without any admin role. + +## Does the bridge maintain state? + +No, the bridge doesn't maintain a state. +However, it keeps an insignificant amount of tokens (dust) in the bridge to reduce gas-costs of future transactions (in case the DUST was sent to the bridge). +By having a dust, we don't need to do a `sstore` from `0` to `non-zero`. + +## Does this bridge maintain state? If so, what is stored and why? + +Yes, this bridge maintain the NFT bought from NFT to relevant user's private identity. +This state is a record for user to redeem their NFT asset. \ No newline at end of file From 269a6e4e488ff0ee3889f1e9985f933c3021e2ee Mon Sep 17 00:00:00 2001 From: SanChuan <2194167956@qq.com> Date: Thu, 5 Jan 2023 12:34:24 +0000 Subject: [PATCH 21/24] copy templete --- src/bridges/nft_trading/NftTradingBridge.sol | 584 +++++++++++++++++++ 1 file changed, 584 insertions(+) create mode 100644 src/bridges/nft_trading/NftTradingBridge.sol diff --git a/src/bridges/nft_trading/NftTradingBridge.sol b/src/bridges/nft_trading/NftTradingBridge.sol new file mode 100644 index 000000000..a87725082 --- /dev/null +++ b/src/bridges/nft_trading/NftTradingBridge.sol @@ -0,0 +1,584 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec +pragma solidity >=0.8.4; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {AztecTypes} from "rollup-encoder/libraries/AztecTypes.sol"; +import {IRollupProcessor} from "rollup-encoder/interfaces/IRollupProcessor.sol"; +import {ErrorLib} from "../base/ErrorLib.sol"; +import {BridgeBase} from "../base/BridgeBase.sol"; +import {ISwapRouter} from "../../interfaces/uniswapv3/ISwapRouter.sol"; +import {IWETH} from "../../interfaces/IWETH.sol"; +import {IQuoter} from "../../interfaces/uniswapv3/IQuoter.sol"; + +/** + * @title Aztec Connect Bridge for Trading on Opensea NFT market + * @author Sanchuan + * @notice You can use this contract to swap tokens on Uniswap v3 along complex paths. + * @dev Encoding of a path allows for up to 2 split paths (see the definition bellow) and up to 3 pools (2 middle + * tokens) in each split path. A path is encoded in _auxData parameter passed to the convert method. _auxData + * carry 64 bits of information. Along with split paths there is a minimum price encoded in auxData. + * + * Each split path takes 19 bits. Minimum price is encoded in 26 bits. Values are placed in the data as follows: + * |26 bits minimum price| |19 bits split path 2| |19 bits split path 1| + * + * Encoding of a split path is: + * |7 bits percentage| |2 bits fee| |3 bits middle token| |2 bits fee| |3 bits middle token| |2 bits fee| + * The meaning of percentage is how much of input amount will be routed through the corresponding split path. + * Fee bits are mapped to specific fee tiers as follows: + * 00 is 0.01%, 01 is 0.05%, 10 is 0.3%, 11 is 1% + * Middle tokens use the following mapping: + * 001 is ETH, 010 is USDC, 011 is USDT, 100 is DAI, 101 is WBTC, 110 is FRAX, 111 is BUSD. + * 000 means the middle token is unused. + * + * Min price is encoded as a floating point number. First 21 bits are used for significand, last 5 bits for + * exponent: |21 bits significand| |5 bits exponent| + * Minimum amount out is computed with the following formula: + * (inputValue * (significand * 10**exponent)) / (10 ** inputAssetDecimals) + * Here are 2 examples. + * 1) If I want to receive 10k Dai for 1 ETH I would set significand to 1 and exponent to 22. + * _totalInputValue = 1e18, asset = ETH (18 decimals), outputAssetA: Dai (18 decimals) + * (1e18 * (1 * 10**22)) / (10**18) = 1e22 --> 10k Dai + * 2) If I want to receive 2000 USDC for 1 ETH, I set significand to 2 and exponent to 9. + * _totalInputValue = 1e18, asset = ETH (18 decimals), outputAssetA: USDC (6 decimals) + * (1e18 * (2 * 10**9)) / (10**18) = 2e9 --> 2000 USDC + * + * Definition of split path: Split path is a term we use when there are multiple (in this case 2) paths between + * which the input amount of tokens is split. As an example we can consider swapping 100 ETH to DAI. In this case + * there could be 2 split paths. 1st split path going through ETH-USDC 500 bps fee pool and USDC-DAI 100 bps fee + * pool and 2nd split path going directly to DAI using the ETH-DAI 500 bps pool. First split path could for + * example consume 80% of input (80 ETH) and the second split path the remaining 20% (20 ETH). + */ +contract UniswapBridge is BridgeBase { + using SafeERC20 for IERC20; + + error InvalidFeeTierEncoding(); + error InvalidFeeTier(); + error InvalidTokenEncoding(); + error InvalidToken(); + error InvalidPercentageAmounts(); + error InsufficientAmountOut(); + error Overflow(); + + // @notice A struct representing a path with 2 split paths. + struct Path { + uint256 percentage1; // Percentage of input to swap through splitPath1 + bytes splitPath1; // A path encoded in a format used by Uniswap's v3 router + uint256 percentage2; // Percentage of input to swap through splitPath2 + bytes splitPath2; // A path encoded in a format used by Uniswap's v3 router + uint256 minPrice; // Minimum acceptable price + } + + struct SplitPath { + uint256 percentage; // Percentage of swap amount to send through this split path + uint256 fee1; // 1st pool fee + address token1; // Address of the 1st pool's output token + uint256 fee2; // 2nd pool fee + address token2; // Address of the 2nd pool's output token + uint256 fee3; // 3rd pool fee + } + + // @dev Event which is emitted when the output token doesn't implement decimals(). + event DefaultDecimalsWarning(); + + ISwapRouter public constant ROUTER = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); + IQuoter public constant QUOTER = IQuoter(0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6); + + // Addresses of middle tokens + address public constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + address public constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + address public constant USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; + address public constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; + address public constant WBTC = 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599; + address public constant FRAX = 0x853d955aCEf822Db058eb8505911ED77F175b99e; + address public constant BUSD = 0x4Fabb145d64652a948d72533023f6E7A623C7C53; + + uint64 public constant SPLIT_PATH_BIT_LENGTH = 19; + uint64 public constant SPLIT_PATHS_BIT_LENGTH = 38; // SPLIT_PATH_BIT_LENGTH * 2 + uint64 public constant PRICE_BIT_LENGTH = 26; // 64 - SPLIT_PATHS_BIT_LENGTH + + // @dev The following masks are used to decode 2 split paths and minimum acceptable price from 1 uint64. + // Binary number 0000000000000000000000000000000000000000000001111111111111111111 (last 19 bits) + uint64 public constant SPLIT_PATH_MASK = 0x7FFFF; + + // Binary number 0000000000000000000000000000000000000011111111111111111111111111 (last 26 bits) + uint64 public constant PRICE_MASK = 0x3FFFFFF; + + // Binary number 0000000000000000000000000000000000000000000000000000000000011111 (last 5 bits) + uint64 public constant EXPONENT_MASK = 0x1F; + + // Binary number 11 + uint64 public constant FEE_MASK = 0x3; + // Binary number 111 + uint64 public constant TOKEN_MASK = 0x7; + + /** + * @notice Set the address of rollup processor. + * @param _rollupProcessor Address of rollup processor + */ + constructor(address _rollupProcessor) BridgeBase(_rollupProcessor) {} + + // @dev Empty method which is present here in order to be able to receive ETH when unwrapping WETH. + receive() external payable {} + + /** + * @notice Sets all the important approvals. + * @param _tokensIn - An array of address of input tokens (tokens to later swap in the convert(...) function) + * @param _tokensOut - An array of address of output tokens (tokens to later return to rollup processor) + * @dev SwapBridge never holds any ERC20 tokens after or before an invocation of any of its functions. For this + * reason the following is not a security risk and makes convert(...) function more gas efficient. + */ + function preApproveTokens(address[] calldata _tokensIn, address[] calldata _tokensOut) external { + uint256 tokensLength = _tokensIn.length; + for (uint256 i; i < tokensLength;) { + address tokenIn = _tokensIn[i]; + // Using safeApprove(...) instead of approve(...) and first setting the allowance to 0 because underlying + // can be Tether + IERC20(tokenIn).safeApprove(address(ROUTER), 0); + IERC20(tokenIn).safeApprove(address(ROUTER), type(uint256).max); + unchecked { + ++i; + } + } + tokensLength = _tokensOut.length; + for (uint256 i; i < tokensLength;) { + address tokenOut = _tokensOut[i]; + // Using safeApprove(...) instead of approve(...) and first setting the allowance to 0 because underlying + // can be Tether + IERC20(tokenOut).safeApprove(address(ROLLUP_PROCESSOR), 0); + IERC20(tokenOut).safeApprove(address(ROLLUP_PROCESSOR), type(uint256).max); + unchecked { + ++i; + } + } + } + + /** + * @notice Registers subsidy criteria for a given token pair. + * @param _tokenIn - Input token to swap + * @param _tokenOut - Output token to swap + */ + function registerSubsidyCriteria(address _tokenIn, address _tokenOut) external { + SUBSIDY.setGasUsageAndMinGasPerMinute({ + _criteria: _computeCriteria(_tokenIn, _tokenOut), + _gasUsage: uint32(300000), // 300k gas (Note: this is a gas usage when only 1 split path is used) + _minGasPerMinute: uint32(100) // 1 fully subsidized call per 2 days (300k / (24 * 60) / 2) + }); + } + + /** + * @notice A function which swaps input token for output token along the path encoded in _auxData. + * @param _inputAssetA - Input ERC20 token + * @param _outputAssetA - Output ERC20 token + * @param _totalInputValue - Amount of input token to swap + * @param _interactionNonce - Interaction nonce + * @param _auxData - Encoded path (gets decoded to Path struct) + * @param _rollupBeneficiary - Address which receives subsidy if the call is eligible for it + * @return outputValueA - The amount of output token received + */ + function convert( + AztecTypes.AztecAsset calldata _inputAssetA, + AztecTypes.AztecAsset calldata, + AztecTypes.AztecAsset calldata _outputAssetA, + AztecTypes.AztecAsset calldata, + uint256 _totalInputValue, + uint256 _interactionNonce, + uint64 _auxData, + address _rollupBeneficiary + ) external payable override (BridgeBase) onlyRollup returns (uint256 outputValueA, uint256, bool) { + // Accumulate subsidy to _rollupBeneficiary + SUBSIDY.claimSubsidy( + _computeCriteria(_inputAssetA.erc20Address, _outputAssetA.erc20Address), _rollupBeneficiary + ); + + bool inputIsEth = _inputAssetA.assetType == AztecTypes.AztecAssetType.ETH; + bool outputIsEth = _outputAssetA.assetType == AztecTypes.AztecAssetType.ETH; + + if (_inputAssetA.assetType != AztecTypes.AztecAssetType.ERC20 && !inputIsEth) { + revert ErrorLib.InvalidInputA(); + } + if (_outputAssetA.assetType != AztecTypes.AztecAssetType.ERC20 && !outputIsEth) { + revert ErrorLib.InvalidOutputA(); + } + + Path memory path = _decodePath( + inputIsEth ? WETH : _inputAssetA.erc20Address, _auxData, outputIsEth ? WETH : _outputAssetA.erc20Address + ); + + uint256 inputValueSplitPath1 = (_totalInputValue * path.percentage1) / 100; + + if (path.percentage1 != 0) { + // Swap using the first swap path + outputValueA = ROUTER.exactInput{value: inputIsEth ? inputValueSplitPath1 : 0}( + ISwapRouter.ExactInputParams({ + path: path.splitPath1, + recipient: address(this), + deadline: block.timestamp, + amountIn: inputValueSplitPath1, + amountOutMinimum: 0 + }) + ); + } + + if (path.percentage2 != 0) { + // Swap using the second swap path + uint256 inputValueSplitPath2 = _totalInputValue - inputValueSplitPath1; + outputValueA += ROUTER.exactInput{value: inputIsEth ? inputValueSplitPath2 : 0}( + ISwapRouter.ExactInputParams({ + path: path.splitPath2, + recipient: address(this), + deadline: block.timestamp, + amountIn: inputValueSplitPath2, + amountOutMinimum: 0 + }) + ); + } + + uint256 tokenInDecimals = 18; + if (!inputIsEth) { + try IERC20Metadata(_inputAssetA.erc20Address).decimals() returns (uint8 decimals) { + tokenInDecimals = decimals; + } catch (bytes memory) { + emit DefaultDecimalsWarning(); + } + } + uint256 amountOutMinimum = (_totalInputValue * path.minPrice) / 10 ** tokenInDecimals; + if (outputValueA < amountOutMinimum) revert InsufficientAmountOut(); + + if (outputIsEth) { + IWETH(WETH).withdraw(outputValueA); + IRollupProcessor(ROLLUP_PROCESSOR).receiveEthFromBridge{value: outputValueA}(_interactionNonce); + } + } + + /** + * @notice A function which encodes path to a format expected in _auxData of this.convert(...) + * @param _amountIn - Amount of tokenIn to swap + * @param _minAmountOut - Amount of tokenOut to receive + * @param _tokenIn - Address of _tokenIn (@dev used only to fetch decimals) + * @param _splitPath1 - Split path to encode + * @param _splitPath2 - Split path to encode + * @return Path encoded in a format expected in _auxData of this.convert(...) + * @dev This function is not optimized and is expected to be used on frontend and in tests. + * @dev Reverts when min price is bigger than max encodeable value. + */ + function encodePath( + uint256 _amountIn, + uint256 _minAmountOut, + address _tokenIn, + SplitPath calldata _splitPath1, + SplitPath calldata _splitPath2 + ) external view returns (uint64) { + if (_splitPath1.percentage + _splitPath2.percentage != 100) revert InvalidPercentageAmounts(); + + return uint64( + ( + _computeEncodedMinPrice(_amountIn, _minAmountOut, IERC20Metadata(_tokenIn).decimals()) + << SPLIT_PATHS_BIT_LENGTH + ) + (_encodeSplitPath(_splitPath1) << SPLIT_PATH_BIT_LENGTH) + _encodeSplitPath(_splitPath2) + ); + } + + /** + * @notice A function which encodes path to a format expected in _auxData of this.convert(...) + * @param _amountIn - Amount of tokenIn to swap + * @param _tokenIn - Address of _tokenIn (@dev used only to fetch decimals) + * @param _path - Split path to encode + * @param _tokenOut - Address of _tokenIn (@dev used only to fetch decimals) + * @return amountOut - + */ + function quote(uint256 _amountIn, address _tokenIn, uint64 _path, address _tokenOut) + external + returns (uint256 amountOut) + { + Path memory path = _decodePath(_tokenIn, _path, _tokenOut); + uint256 inputValueSplitPath1 = (_amountIn * path.percentage1) / 100; + + if (path.percentage1 != 0) { + // Swap using the first swap path + amountOut += QUOTER.quoteExactInput(path.splitPath1, inputValueSplitPath1); + } + + if (path.percentage2 != 0) { + // Swap using the second swap path + amountOut += QUOTER.quoteExactInput(path.splitPath2, _amountIn - inputValueSplitPath1); + } + } + + /** + * @notice Computes the criteria that is passed when claiming subsidy. + * @param _inputAssetA The input asset + * @param _outputAssetA The output asset + * @return The criteria + */ + function computeCriteria( + AztecTypes.AztecAsset calldata _inputAssetA, + AztecTypes.AztecAsset calldata, + AztecTypes.AztecAsset calldata _outputAssetA, + AztecTypes.AztecAsset calldata, + uint64 + ) public pure override (BridgeBase) returns (uint256) { + return _computeCriteria(_inputAssetA.erc20Address, _outputAssetA.erc20Address); + } + + function _computeCriteria(address _inputToken, address _outputToken) internal pure returns (uint256) { + return uint256(keccak256(abi.encodePacked(_inputToken, _outputToken))); + } + + /** + * @notice A function which computes min price and encodes it in the format used in this bridge. + * @param _amountIn - Amount of tokenIn to swap + * @param _minAmountOut - Amount of tokenOut to receive + * @param _tokenInDecimals - Number of decimals of tokenIn + * @return encodedMinPrice - Min acceptable encoded in a format used in this bridge. + * @dev This function is not optimized and is expected to be used on frontend and in tests. + * @dev Reverts when min price is bigger than max encodeable value. + */ + function _computeEncodedMinPrice(uint256 _amountIn, uint256 _minAmountOut, uint256 _tokenInDecimals) + internal + pure + returns (uint256 encodedMinPrice) + { + uint256 minPrice = (_minAmountOut * 10 ** _tokenInDecimals) / _amountIn; + // 2097151 = 2**21 - 1 --> this number and its multiples of 10 can be encoded without precision loss + if (minPrice <= 2097151) { + // minPrice is smaller than the boundary of significand --> significand = _x, exponent = 0 + encodedMinPrice = minPrice << 5; + } else { + uint256 exponent = 0; + while (minPrice > 2097151) { + minPrice /= 10; + ++exponent; + // 31 = 2**5 - 1 --> max exponent + if (exponent > 31) revert Overflow(); + } + encodedMinPrice = (minPrice << 5) + exponent; + } + } + + /** + * @notice A function which encodes a split path. + * @param _path - Split path to encode + * @return Encoded split path (in the last 19 bits of uint256) + * @dev In place of unused middle tokens leave address(0). + * @dev Fee tier corresponding to unused middle token is ignored. + */ + function _encodeSplitPath(SplitPath calldata _path) internal pure returns (uint256) { + if (_path.percentage == 0) return 0; + return (_path.percentage << 12) + (_encodeFeeTier(_path.fee1) << 10) + (_encodeMiddleToken(_path.token1) << 7) + + (_encodeFeeTier(_path.fee2) << 5) + (_encodeMiddleToken(_path.token2) << 2) + (_encodeFeeTier(_path.fee3)); + } + + /** + * @notice A function which encodes fee tier. + * @param _feeTier - Fee tier in bps + * @return Encoded fee tier (in the last 2 bits of uint256) + */ + function _encodeFeeTier(uint256 _feeTier) internal pure returns (uint256) { + if (_feeTier == 100) { + // Binary number 00 + return 0; + } + if (_feeTier == 500) { + // Binary number 01 + return 1; + } + if (_feeTier == 3000) { + // Binary number 10 + return 2; + } + if (_feeTier == 10000) { + // Binary number 11 + return 3; + } + revert InvalidFeeTier(); + } + + /** + * @notice A function which returns token encoding for a given token address. + * @param _token - Token address + * @return encodedToken - Encoded token (in the last 3 bits of uint256) + */ + function _encodeMiddleToken(address _token) internal pure returns (uint256 encodedToken) { + if (_token == address(0)) { + // unused token + return 0; + } + if (_token == WETH) { + // binary number 001 + return 1; + } + if (_token == USDC) { + // binary number 010 + return 2; + } + if (_token == USDT) { + // binary number 011 + return 3; + } + if (_token == DAI) { + // binary number 100 + return 4; + } + if (_token == WBTC) { + // binary number 101 + return 5; + } + if (_token == FRAX) { + // binary number 110 + return 6; + } + if (_token == BUSD) { + // binary number 111 + return 7; + } + revert InvalidToken(); + } + + /** + * @notice A function which deserializes encoded path to Path struct. + * @param _tokenIn - Input ERC20 token + * @param _encodedPath - Encoded path + * @param _tokenOut - Output ERC20 token + * @return path - Decoded/deserialized path struct + */ + function _decodePath(address _tokenIn, uint256 _encodedPath, address _tokenOut) + internal + pure + returns (Path memory path) + { + (uint256 percentage1, bytes memory splitPath1) = + _decodeSplitPath(_tokenIn, _encodedPath & SPLIT_PATH_MASK, _tokenOut); + path.percentage1 = percentage1; + path.splitPath1 = splitPath1; + + (uint256 percentage2, bytes memory splitPath2) = + _decodeSplitPath(_tokenIn, (_encodedPath >> SPLIT_PATH_BIT_LENGTH) & SPLIT_PATH_MASK, _tokenOut); + + if (percentage1 + percentage2 != 100) revert InvalidPercentageAmounts(); + + path.percentage2 = percentage2; + path.splitPath2 = splitPath2; + path.minPrice = _decodeMinPrice(_encodedPath >> SPLIT_PATHS_BIT_LENGTH); + } + + /** + * @notice A function which returns a percentage of input going through the split path and the split path encoded + * in a format compatible with Uniswap router. + * @param _tokenIn - Input ERC20 token + * @param _encodedSplitPath - Encoded split path (in the last 19 bits of uint256) + * @param _tokenOut - Output ERC20 token + * @return percentage - A percentage of input going through the corresponding split path + * @return splitPath - A split path encoded in a format compatible with Uniswap router + */ + function _decodeSplitPath(address _tokenIn, uint256 _encodedSplitPath, address _tokenOut) + internal + pure + returns (uint256 percentage, bytes memory splitPath) + { + uint256 fee3 = _encodedSplitPath & FEE_MASK; + uint256 middleToken2 = (_encodedSplitPath >> 2) & TOKEN_MASK; + uint256 fee2 = (_encodedSplitPath >> 5) & FEE_MASK; + uint256 middleToken1 = (_encodedSplitPath >> 7) & TOKEN_MASK; + uint256 fee1 = (_encodedSplitPath >> 10) & FEE_MASK; + percentage = _encodedSplitPath >> 12; + + if (middleToken1 != 0 && middleToken2 != 0) { + splitPath = abi.encodePacked( + _tokenIn, + _decodeFeeTier(fee1), + _decodeMiddleToken(middleToken1), + _decodeFeeTier(fee2), + _decodeMiddleToken(middleToken2), + _decodeFeeTier(fee3), + _tokenOut + ); + } else if (middleToken1 != 0) { + splitPath = abi.encodePacked( + _tokenIn, _decodeFeeTier(fee1), _decodeMiddleToken(middleToken1), _decodeFeeTier(fee3), _tokenOut + ); + } else if (middleToken2 != 0) { + splitPath = abi.encodePacked( + _tokenIn, _decodeFeeTier(fee2), _decodeMiddleToken(middleToken2), _decodeFeeTier(fee3), _tokenOut + ); + } else { + splitPath = abi.encodePacked(_tokenIn, _decodeFeeTier(fee3), _tokenOut); + } + } + + /** + * @notice A function which converts minimum price in a floating point format to integer. + * @param _encodedMinPrice - Encoded minimum price (in the last 26 bits of uint256) + * @return minPrice - Minimum acceptable price represented as an integer + */ + function _decodeMinPrice(uint256 _encodedMinPrice) internal pure returns (uint256 minPrice) { + // 21 bits significand, 5 bits exponent + uint256 significand = _encodedMinPrice >> 5; + uint256 exponent = _encodedMinPrice & EXPONENT_MASK; + minPrice = significand * 10 ** exponent; + } + + /** + * @notice A function which converts encoded fee tier to a fee tier in an integer format. + * @param _encodedFeeTier - Encoded fee tier (in the last 2 bits of uint256) + * @return feeTier - Decoded fee tier in an integer format + */ + function _decodeFeeTier(uint256 _encodedFeeTier) internal pure returns (uint24 feeTier) { + if (_encodedFeeTier == 0) { + // Binary number 00 + return uint24(100); + } + if (_encodedFeeTier == 1) { + // Binary number 01 + return uint24(500); + } + if (_encodedFeeTier == 2) { + // Binary number 10 + return uint24(3000); + } + if (_encodedFeeTier == 3) { + // Binary number 11 + return uint24(10000); + } + revert InvalidFeeTierEncoding(); + } + + /** + * @notice A function which returns token address for an encoded token. + * @param _encodedToken - Encoded token (in the last 3 bits of uint256) + * @return token - Token address + */ + function _decodeMiddleToken(uint256 _encodedToken) internal pure returns (address token) { + if (_encodedToken == 1) { + // binary number 001 + return WETH; + } + if (_encodedToken == 2) { + // binary number 010 + return USDC; + } + if (_encodedToken == 3) { + // binary number 011 + return USDT; + } + if (_encodedToken == 4) { + // binary number 100 + return DAI; + } + if (_encodedToken == 5) { + // binary number 101 + return WBTC; + } + if (_encodedToken == 6) { + // binary number 110 + return FRAX; + } + if (_encodedToken == 7) { + // binary number 111 + return BUSD; + } + revert InvalidTokenEncoding(); + } +} From dc9c463fd9a1ad7683abc5ea4dbbd0704d215e96 Mon Sep 17 00:00:00 2001 From: SanChuan <2194167956@qq.com> Date: Thu, 5 Jan 2023 16:07:13 +0000 Subject: [PATCH 22/24] learn test --- src/test/bridges/nft_trading/ExampleE2E.t.sol | 109 ++++++++++++++++ .../bridges/nft_trading/ExampleUnit.t.sol | 119 ++++++++++++++++++ 2 files changed, 228 insertions(+) create mode 100644 src/test/bridges/nft_trading/ExampleE2E.t.sol create mode 100644 src/test/bridges/nft_trading/ExampleUnit.t.sol diff --git a/src/test/bridges/nft_trading/ExampleE2E.t.sol b/src/test/bridges/nft_trading/ExampleE2E.t.sol new file mode 100644 index 000000000..9f64f7f84 --- /dev/null +++ b/src/test/bridges/nft_trading/ExampleE2E.t.sol @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec. +pragma solidity >=0.8.4; + +import {BridgeTestBase} from "./../../aztec/base/BridgeTestBase.sol"; +import {AztecTypes} from "rollup-encoder/libraries/AztecTypes.sol"; + +// Example-specific imports +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ExampleBridge} from "../../../bridges/example/ExampleBridge.sol"; +import {ErrorLib} from "../../../bridges/base/ErrorLib.sol"; + +/** + * @notice The purpose of this test is to test the bridge in an environment that is as close to the final deployment + * as possible without spinning up all the rollup infrastructure (sequencer, proof generator etc.). + */ +contract ExampleE2ETest is BridgeTestBase { + address public constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + address private constant BENEFICIARY = address(11); + + // The reference to the example bridge + ExampleBridge internal bridge; + // To store the id of the example bridge after being added + uint256 private id; + + function setUp() public { + // Deploy a new example bridge + bridge = new ExampleBridge(address(ROLLUP_PROCESSOR)); + + // Use the label cheatcode to mark the address with "Example Bridge" in the traces + vm.label(address(bridge), "Example Bridge"); + + // Impersonate the multi-sig to add a new bridge + vm.startPrank(MULTI_SIG); + + // List the example-bridge with a gasLimit of 120k + // WARNING: If you set this value too low the interaction will fail for seemingly no reason! + // OTOH if you se it too high bridge users will pay too much + ROLLUP_PROCESSOR.setSupportedBridge(address(bridge), 120000); + + // List USDC with a gasLimit of 100k + // Note: necessary for assets which are not already registered on RollupProcessor + // Call https://etherscan.io/address/0xFF1F2B4ADb9dF6FC8eAFecDcbF96A2B351680455#readProxyContract#F25 to get + // addresses of all the listed ERC20 tokens + ROLLUP_PROCESSOR.setSupportedAsset(USDC, 100000); + + vm.stopPrank(); + + // Fetch the id of the example bridge + id = ROLLUP_PROCESSOR.getSupportedBridgesLength(); + + // Subsidize the bridge when used with USDC and register a beneficiary + AztecTypes.AztecAsset memory usdcAsset = ROLLUP_ENCODER.getRealAztecAsset(USDC); + uint256 criteria = bridge.computeCriteria(usdcAsset, emptyAsset, usdcAsset, emptyAsset, 0); + uint32 gasPerMinute = 200; + SUBSIDY.subsidize{value: 1 ether}(address(bridge), criteria, gasPerMinute); + + SUBSIDY.registerBeneficiary(BENEFICIARY); + + // Set the rollupBeneficiary on BridgeTestBase so that it gets included in the proofData + ROLLUP_ENCODER.setRollupBeneficiary(BENEFICIARY); + } + + // @dev In order to avoid overflows we set _depositAmount to be uint96 instead of uint256. + function testExampleBridgeE2ETest(uint96 _depositAmount) public { + vm.assume(_depositAmount > 1); + vm.warp(block.timestamp + 1 days); + + // Use the helper function to fetch the support AztecAsset for DAI + AztecTypes.AztecAsset memory usdcAsset = ROLLUP_ENCODER.getRealAztecAsset(address(USDC)); + + // Mint the depositAmount of Dai to rollupProcessor + deal(USDC, address(ROLLUP_PROCESSOR), _depositAmount); + + // Computes the encoded data for the specific bridge interaction + ROLLUP_ENCODER.defiInteractionL2(id, usdcAsset, emptyAsset, usdcAsset, emptyAsset, 0, _depositAmount); + + // Execute the rollup with the bridge interaction. Ensure that event as seen above is emitted. + (uint256 outputValueA, uint256 outputValueB, bool isAsync) = ROLLUP_ENCODER.processRollupAndGetBridgeResult(); + + // Note: Unlike in unit tests there is no need to manually transfer the tokens - RollupProcessor does this + + // Check the output values are as expected + assertEq(outputValueA, _depositAmount, "outputValueA doesn't equal deposit"); + assertEq(outputValueB, 0, "Non-zero outputValueB"); + assertFalse(isAsync, "Bridge is not synchronous"); + + // Check that the balance of the rollup is same as before interaction (bridge just sends funds back) + assertEq(_depositAmount, IERC20(USDC).balanceOf(address(ROLLUP_PROCESSOR)), "Balances must match"); + + // Perform a second rollup with half the deposit, perform similar checks. + uint256 secondDeposit = _depositAmount / 2; + + ROLLUP_ENCODER.defiInteractionL2(id, usdcAsset, emptyAsset, usdcAsset, emptyAsset, 0, secondDeposit); + + // Execute the rollup with the bridge interaction. Ensure that event as seen above is emitted. + (outputValueA, outputValueB, isAsync) = ROLLUP_ENCODER.processRollupAndGetBridgeResult(); + + // Check the output values are as expected + assertEq(outputValueA, secondDeposit, "outputValueA doesn't equal second deposit"); + assertEq(outputValueB, 0, "Non-zero outputValueB"); + assertFalse(isAsync, "Bridge is not synchronous"); + + // Check that the balance of the rollup is same as before interaction (bridge just sends funds back) + assertEq(_depositAmount, IERC20(USDC).balanceOf(address(ROLLUP_PROCESSOR)), "Balances must match"); + + assertGt(SUBSIDY.claimableAmount(BENEFICIARY), 0, "Claimable was not updated"); + } +} diff --git a/src/test/bridges/nft_trading/ExampleUnit.t.sol b/src/test/bridges/nft_trading/ExampleUnit.t.sol new file mode 100644 index 000000000..0c6aeb464 --- /dev/null +++ b/src/test/bridges/nft_trading/ExampleUnit.t.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec. +pragma solidity >=0.8.4; + +import {BridgeTestBase} from "./../../aztec/base/BridgeTestBase.sol"; +import {AztecTypes} from "rollup-encoder/libraries/AztecTypes.sol"; + +// Example-specific imports +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ExampleBridge} from "../../../bridges/example/ExampleBridge.sol"; +import {ErrorLib} from "../../../bridges/base/ErrorLib.sol"; + +// @notice The purpose of this test is to directly test convert functionality of the bridge. +contract ExampleUnitTest is BridgeTestBase { + address private constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; + address private constant BENEFICIARY = address(11); + + address private rollupProcessor; + // The reference to the example bridge + ExampleBridge private bridge; + + // @dev This method exists on RollupProcessor.sol. It's defined here in order to be able to receive ETH like a real + // rollup processor would. + function receiveEthFromBridge(uint256 _interactionNonce) external payable {} + + function setUp() public { + // In unit tests we set address of rollupProcessor to the address of this test contract + rollupProcessor = address(this); + + // Deploy a new example bridge + bridge = new ExampleBridge(rollupProcessor); + + // Set ETH balance of bridge and BENEFICIARY to 0 for clarity (somebody sent ETH to that address on mainnet) + vm.deal(address(bridge), 0); + vm.deal(BENEFICIARY, 0); + + // Use the label cheatcode to mark the address with "Example Bridge" in the traces + vm.label(address(bridge), "Example Bridge"); + + // Subsidize the bridge when used with Dai and register a beneficiary + AztecTypes.AztecAsset memory daiAsset = ROLLUP_ENCODER.getRealAztecAsset(DAI); + uint256 criteria = bridge.computeCriteria(daiAsset, emptyAsset, daiAsset, emptyAsset, 0); + uint32 gasPerMinute = 200; + SUBSIDY.subsidize{value: 1 ether}(address(bridge), criteria, gasPerMinute); + + SUBSIDY.registerBeneficiary(BENEFICIARY); + } + + function testInvalidCaller(address _callerAddress) public { + vm.assume(_callerAddress != rollupProcessor); + // Use HEVM cheatcode to call from a different address than is address(this) + vm.prank(_callerAddress); + vm.expectRevert(ErrorLib.InvalidCaller.selector); + bridge.convert(emptyAsset, emptyAsset, emptyAsset, emptyAsset, 0, 0, 0, address(0)); + } + + function testInvalidInputAssetType() public { + vm.expectRevert(ErrorLib.InvalidInputA.selector); + bridge.convert(emptyAsset, emptyAsset, emptyAsset, emptyAsset, 0, 0, 0, address(0)); + } + + function testInvalidOutputAssetType() public { + AztecTypes.AztecAsset memory inputAssetA = + AztecTypes.AztecAsset({id: 1, erc20Address: DAI, assetType: AztecTypes.AztecAssetType.ERC20}); + vm.expectRevert(ErrorLib.InvalidOutputA.selector); + bridge.convert(inputAssetA, emptyAsset, emptyAsset, emptyAsset, 0, 0, 0, address(0)); + } + + function testExampleBridgeUnitTestFixed() public { + testExampleBridgeUnitTest(10 ether); + } + + // @notice The purpose of this test is to directly test convert functionality of the bridge. + // @dev In order to avoid overflows we set _depositAmount to be uint96 instead of uint256. + function testExampleBridgeUnitTest(uint96 _depositAmount) public { + vm.warp(block.timestamp + 1 days); + + // Define input and output assets + AztecTypes.AztecAsset memory inputAssetA = + AztecTypes.AztecAsset({id: 1, erc20Address: DAI, assetType: AztecTypes.AztecAssetType.ERC20}); + + AztecTypes.AztecAsset memory outputAssetA = inputAssetA; + + // Rollup processor transfers ERC20 tokens to the bridge before calling convert. Since we are calling + // bridge.convert(...) function directly we have to transfer the funds in the test on our own. In this case + // we'll solve it by directly minting the _depositAmount of Dai to the bridge. + deal(DAI, address(bridge), _depositAmount); + + // Store dai balance before interaction to be able to verify the balance after interaction is correct + uint256 daiBalanceBefore = IERC20(DAI).balanceOf(rollupProcessor); + + (uint256 outputValueA, uint256 outputValueB, bool isAsync) = bridge.convert( + inputAssetA, // _inputAssetA - definition of an input asset + emptyAsset, // _inputAssetB - not used so can be left empty + outputAssetA, // _outputAssetA - in this example equal to input asset + emptyAsset, // _outputAssetB - not used so can be left empty + _depositAmount, // _totalInputValue - an amount of input asset A sent to the bridge + 0, // _interactionNonce + 0, // _auxData - not used in the example bridge + BENEFICIARY // _rollupBeneficiary - address, the subsidy will be sent to + ); + + // Now we transfer the funds back from the bridge to the rollup processor + // In this case input asset equals output asset so I only work with the input asset definition + // Basically in all the real world use-cases output assets would differ from input assets + IERC20(inputAssetA.erc20Address).transferFrom(address(bridge), rollupProcessor, outputValueA); + + assertEq(outputValueA, _depositAmount, "Output value A doesn't equal deposit amount"); + assertEq(outputValueB, 0, "Output value B is not 0"); + assertTrue(!isAsync, "Bridge is incorrectly in an async mode"); + + uint256 daiBalanceAfter = IERC20(DAI).balanceOf(rollupProcessor); + + assertEq(daiBalanceAfter - daiBalanceBefore, _depositAmount, "Balances must match"); + + SUBSIDY.withdraw(BENEFICIARY); + assertGt(BENEFICIARY.balance, 0, "Subsidy was not claimed"); + } +} From a461f25f463bb94b8d1793773814daf5b816eb4e Mon Sep 17 00:00:00 2001 From: SanChuan <2194167956@qq.com> Date: Fri, 6 Jan 2023 03:20:57 +0000 Subject: [PATCH 23/24] update specs flow --- specs/bridges/nft_trading/readme.md | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/specs/bridges/nft_trading/readme.md b/specs/bridges/nft_trading/readme.md index 1e226df85..85d463c89 100644 --- a/specs/bridges/nft_trading/readme.md +++ b/specs/bridges/nft_trading/readme.md @@ -7,28 +7,36 @@ In this way, the owner of one NFT owner is in private with the power of Aztec's ## What protocol(s) does the bridge interact with ? - - The bridge interacts with [OpenSea](https://opensea.io/). ## What is the flow of the bridge? -The simple flow as below: + +The NFT bridge will design as a Wrap Erc721 Contract, that means everytime when the contract received a NFT, it will mint a relevant Wrapped NFT to this user, This Wrapped NFT can be transfer to Aztec L2, also can be unwrap and redeem the original NFT anytimes. + +The `BUY` flow as below: 1. User A on Aztec chain bridge interface pay and buy an NFT on Opensea in Ethereum. 2. The "Buy" order and relevant fee is send to L1 by Aztec's rollup. 3. The bridge contract in L1 is triggered to interact with OpenSea protocol. -4. Then the bridge contract own this NFT and record it to a private user id. -5. The User A On Aztec can redeem this NFT anytime, with his valid signature. - +4. Then the bridge contract own this NFT and Mint a Wrapped NFT for Aztec L2 to bridge. +5. The User A On Aztec Owned this Wrapped NFT. +The `REDEEM` flow as below: +1. User A hold an wrapped NFT in Aztec L2. +2. He call the bridge to unwrapped and send the real NFT to a L1 address. +3. The Bridge contract is trigged, and send the resl NFT to the user specifed address, and burn the wrapped NFT. ### General Properties of convert(...) function + The `AztecTypes.AztecAsset calldata _inputAssetA` should be specificed as the Bridge's wrapped NFT. + And the relevant function calling is encoded into the `_auxData` Info. + In the Bridge contract, the function will be routed by the decoded +`_auxData`. - The bridge is synchronous, and will always return `isAsync = false`. - The bridge uses `_auxData` to encode the target NFT, id, price. -- The Bridge perform token pre-approvals to allow the `ROLLUP_PROCESSOR` and `UNI_ROUTER` to pull tokens from it. - This is to reduce gas-overhead when performing the actions. It is safe to do, as the bridge is not holding the funds itself. +- The Bridge perform token pre-approvals to allow the `ROLLUP_PROCESSOR` to pull tokens from it. + ## Is the contract upgradeable? From bd3772525eff9edf5ad35d1ffd2ecc8f4f7f73a1 Mon Sep 17 00:00:00 2001 From: sc <2194167956@qq.com> Date: Fri, 6 Jan 2023 22:45:49 +0800 Subject: [PATCH 24/24] add NFT Transfer templete Refers to: https://github.com/critesjosh/aztec-connect-starter/tree/nft-bridge Kudo to this guy. --- .gitignore | 3 +- src/bridges/nft-basic/NFTVault.sol | 135 +++++++++ src/bridges/nft_trading/NFTVault.sol | 135 +++++++++ src/bridges/registry/AddressRegistry.sol | 87 ++++++ .../nft-basic/NFTVaultDeployment.s.sol | 42 +++ .../registry/AddressRegistryDeployment.s.sol | 28 ++ .../bridges/nft-basic/NFTVaultBasicE2E.t.sol | 157 +++++++++++ .../bridges/nft-basic/NFTVaultBasicUnit.t.sol | 257 ++++++++++++++++++ .../bridges/registry/AddressRegistryE2E.t.sol | 76 ++++++ .../registry/AddressRegistryUnitTest.t.sol | 127 +++++++++ 10 files changed, 1046 insertions(+), 1 deletion(-) create mode 100644 src/bridges/nft-basic/NFTVault.sol create mode 100644 src/bridges/nft_trading/NFTVault.sol create mode 100644 src/bridges/registry/AddressRegistry.sol create mode 100644 src/deployment/nft-basic/NFTVaultDeployment.s.sol create mode 100644 src/deployment/registry/AddressRegistryDeployment.s.sol create mode 100644 src/test/bridges/nft-basic/NFTVaultBasicE2E.t.sol create mode 100644 src/test/bridges/nft-basic/NFTVaultBasicUnit.t.sol create mode 100644 src/test/bridges/registry/AddressRegistryE2E.t.sol create mode 100644 src/test/bridges/registry/AddressRegistryUnitTest.t.sol diff --git a/.gitignore b/.gitignore index 4b6e85fbc..3d60e9478 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ out/ node_modules/ yarn-error.log typechain-types/ -broadcast/ \ No newline at end of file +broadcast/ +compose-dev.yaml diff --git a/src/bridges/nft-basic/NFTVault.sol b/src/bridges/nft-basic/NFTVault.sol new file mode 100644 index 000000000..dec468119 --- /dev/null +++ b/src/bridges/nft-basic/NFTVault.sol @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec. +pragma solidity >=0.8.4; + +import {IERC721} from "../../../lib/openzeppelin-contracts/contracts/interfaces/IERC721.sol"; +import {AztecTypes} from "../../../lib/rollup-encoder/src/libraries/AztecTypes.sol"; +import {ErrorLib} from "../base/ErrorLib.sol"; +import {BridgeBase} from "../base/BridgeBase.sol"; +import {AddressRegistry} from "../registry/AddressRegistry.sol"; + +/** + * @title Basic NFT Vault for Aztec. + * @author Josh Crites, (@critesjosh on Github), Aztec Team + * @notice You can use this contract to hold your NFTs on Aztec. Whoever holds the corresponding virutal asset note can withdraw the NFT. + * @dev This bridge demonstrates basic functionality for an NFT bridge. This may be extended to support more features. + */ +contract NFTVault is BridgeBase { + struct NFTAsset { + address collection; + uint256 tokenId; + } + + AddressRegistry public immutable REGISTRY; + + mapping(uint256 => NFTAsset) public nftAssets; + + error InvalidVirtualAssetId(); + + event NFTDeposit(uint256 indexed virtualAssetId, address indexed collection, uint256 indexed tokenId); + event NFTWithdraw(uint256 indexed virtualAssetId, address indexed collection, uint256 indexed tokenId); + + /** + * @notice Set the addresses of RollupProcessor and AddressRegistry + * @param _rollupProcessor Address of the RollupProcessor + * @param _registry Address of the AddressRegistry + */ + constructor(address _rollupProcessor, address _registry) BridgeBase(_rollupProcessor) { + REGISTRY = AddressRegistry(_registry); + } + + /** + * @notice Function for the first step of a NFT deposit, a NFT withdrawal, or transfer to another NFTVault. + * @dev This method can only be called from the RollupProcessor. The first step of the + * deposit flow returns a virutal asset note that will represent the NFT on Aztec. After the + * virutal asset note is received on Aztec, the user calls matchAndPull which deposits the NFT + * into Aztec and matches it with the virtual asset. When the virutal asset is sent to this function + * it is burned and the NFT is sent to the recipient passed in _auxData. + * + * @param _inputAssetA - ETH (Deposit) or VIRTUAL (Withdrawal) + * @param _outputAssetA - VIRTUAL (Deposit) or 0 ETH (Withdrawal) + * @param _totalInputValue - must be 1 wei (Deposit) or 1 VIRTUAL (Withdrawal) + * @param _interactionNonce - A globally unique identifier of this interaction/`convert(...)` call + * corresponding to the returned virtual asset id + * @param _auxData - corresponds to the Ethereum address id in the AddressRegistry.sol for withdrawals + * @return outputValueA - 1 VIRTUAL asset (Deposit) or 0 ETH (Withdrawal) + * + */ + + function convert( + AztecTypes.AztecAsset calldata _inputAssetA, + AztecTypes.AztecAsset calldata, + AztecTypes.AztecAsset calldata _outputAssetA, + AztecTypes.AztecAsset calldata, + uint256 _totalInputValue, + uint256 _interactionNonce, + uint64 _auxData, + address + ) + external + payable + override (BridgeBase) + onlyRollup + returns (uint256 outputValueA, uint256 outputValueB, bool isAsync) + { + if ( + _inputAssetA.assetType == AztecTypes.AztecAssetType.NOT_USED + || _inputAssetA.assetType == AztecTypes.AztecAssetType.ERC20 + ) revert ErrorLib.InvalidInputA(); + if ( + _outputAssetA.assetType == AztecTypes.AztecAssetType.NOT_USED + || _outputAssetA.assetType == AztecTypes.AztecAssetType.ERC20 + ) revert ErrorLib.InvalidOutputA(); + if (_totalInputValue != 1) { + revert ErrorLib.InvalidInputAmount(); + } + if ( + _inputAssetA.assetType == AztecTypes.AztecAssetType.ETH + && _outputAssetA.assetType == AztecTypes.AztecAssetType.VIRTUAL + ) { + return (1, 0, false); + } else if (_inputAssetA.assetType == AztecTypes.AztecAssetType.VIRTUAL) { + NFTAsset memory token = nftAssets[_inputAssetA.id]; + if (token.collection == address(0x0)) { + revert ErrorLib.InvalidInputA(); + } + + address to = REGISTRY.addresses(_auxData); + if (to == address(0x0)) { + revert ErrorLib.InvalidAuxData(); + } + delete nftAssets[_inputAssetA.id]; + emit NFTWithdraw(_inputAssetA.id, token.collection, token.tokenId); + + if (_outputAssetA.assetType == AztecTypes.AztecAssetType.ETH) { + IERC721(token.collection).transferFrom(address(this), to, token.tokenId); + return (0, 0, false); + } else { + IERC721(token.collection).approve(to, token.tokenId); + NFTVault(to).matchAndPull(_interactionNonce, token.collection, token.tokenId); + return (1, 0, false); + } + } + } + + /** + * @notice Function for the second step of a NFT deposit or for transfers from other NFTVaults. + * @dev For a deposit, this method is called by an Ethereum L1 account that owns the NFT to deposit. + * The user must approve this bridge contract to transfer the users NFT before this function + * is called. This function assumes the NFT contract complies with the ERC721 standard. + * For a transfer from another NFTVault, this method is called by the NFTVault that is sending the NFT. + * + * @param _virtualAssetId - the virutal asset id of the note returned in the deposit step of the convert function + * @param _collection - collection address of the NFT + * @param _tokenId - the token id of the NFT + */ + + function matchAndPull(uint256 _virtualAssetId, address _collection, uint256 _tokenId) external { + if (nftAssets[_virtualAssetId].collection != address(0x0)) { + revert InvalidVirtualAssetId(); + } + nftAssets[_virtualAssetId] = NFTAsset({collection: _collection, tokenId: _tokenId}); + IERC721(_collection).transferFrom(msg.sender, address(this), _tokenId); + emit NFTDeposit(_virtualAssetId, _collection, _tokenId); + } +} diff --git a/src/bridges/nft_trading/NFTVault.sol b/src/bridges/nft_trading/NFTVault.sol new file mode 100644 index 000000000..dec468119 --- /dev/null +++ b/src/bridges/nft_trading/NFTVault.sol @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec. +pragma solidity >=0.8.4; + +import {IERC721} from "../../../lib/openzeppelin-contracts/contracts/interfaces/IERC721.sol"; +import {AztecTypes} from "../../../lib/rollup-encoder/src/libraries/AztecTypes.sol"; +import {ErrorLib} from "../base/ErrorLib.sol"; +import {BridgeBase} from "../base/BridgeBase.sol"; +import {AddressRegistry} from "../registry/AddressRegistry.sol"; + +/** + * @title Basic NFT Vault for Aztec. + * @author Josh Crites, (@critesjosh on Github), Aztec Team + * @notice You can use this contract to hold your NFTs on Aztec. Whoever holds the corresponding virutal asset note can withdraw the NFT. + * @dev This bridge demonstrates basic functionality for an NFT bridge. This may be extended to support more features. + */ +contract NFTVault is BridgeBase { + struct NFTAsset { + address collection; + uint256 tokenId; + } + + AddressRegistry public immutable REGISTRY; + + mapping(uint256 => NFTAsset) public nftAssets; + + error InvalidVirtualAssetId(); + + event NFTDeposit(uint256 indexed virtualAssetId, address indexed collection, uint256 indexed tokenId); + event NFTWithdraw(uint256 indexed virtualAssetId, address indexed collection, uint256 indexed tokenId); + + /** + * @notice Set the addresses of RollupProcessor and AddressRegistry + * @param _rollupProcessor Address of the RollupProcessor + * @param _registry Address of the AddressRegistry + */ + constructor(address _rollupProcessor, address _registry) BridgeBase(_rollupProcessor) { + REGISTRY = AddressRegistry(_registry); + } + + /** + * @notice Function for the first step of a NFT deposit, a NFT withdrawal, or transfer to another NFTVault. + * @dev This method can only be called from the RollupProcessor. The first step of the + * deposit flow returns a virutal asset note that will represent the NFT on Aztec. After the + * virutal asset note is received on Aztec, the user calls matchAndPull which deposits the NFT + * into Aztec and matches it with the virtual asset. When the virutal asset is sent to this function + * it is burned and the NFT is sent to the recipient passed in _auxData. + * + * @param _inputAssetA - ETH (Deposit) or VIRTUAL (Withdrawal) + * @param _outputAssetA - VIRTUAL (Deposit) or 0 ETH (Withdrawal) + * @param _totalInputValue - must be 1 wei (Deposit) or 1 VIRTUAL (Withdrawal) + * @param _interactionNonce - A globally unique identifier of this interaction/`convert(...)` call + * corresponding to the returned virtual asset id + * @param _auxData - corresponds to the Ethereum address id in the AddressRegistry.sol for withdrawals + * @return outputValueA - 1 VIRTUAL asset (Deposit) or 0 ETH (Withdrawal) + * + */ + + function convert( + AztecTypes.AztecAsset calldata _inputAssetA, + AztecTypes.AztecAsset calldata, + AztecTypes.AztecAsset calldata _outputAssetA, + AztecTypes.AztecAsset calldata, + uint256 _totalInputValue, + uint256 _interactionNonce, + uint64 _auxData, + address + ) + external + payable + override (BridgeBase) + onlyRollup + returns (uint256 outputValueA, uint256 outputValueB, bool isAsync) + { + if ( + _inputAssetA.assetType == AztecTypes.AztecAssetType.NOT_USED + || _inputAssetA.assetType == AztecTypes.AztecAssetType.ERC20 + ) revert ErrorLib.InvalidInputA(); + if ( + _outputAssetA.assetType == AztecTypes.AztecAssetType.NOT_USED + || _outputAssetA.assetType == AztecTypes.AztecAssetType.ERC20 + ) revert ErrorLib.InvalidOutputA(); + if (_totalInputValue != 1) { + revert ErrorLib.InvalidInputAmount(); + } + if ( + _inputAssetA.assetType == AztecTypes.AztecAssetType.ETH + && _outputAssetA.assetType == AztecTypes.AztecAssetType.VIRTUAL + ) { + return (1, 0, false); + } else if (_inputAssetA.assetType == AztecTypes.AztecAssetType.VIRTUAL) { + NFTAsset memory token = nftAssets[_inputAssetA.id]; + if (token.collection == address(0x0)) { + revert ErrorLib.InvalidInputA(); + } + + address to = REGISTRY.addresses(_auxData); + if (to == address(0x0)) { + revert ErrorLib.InvalidAuxData(); + } + delete nftAssets[_inputAssetA.id]; + emit NFTWithdraw(_inputAssetA.id, token.collection, token.tokenId); + + if (_outputAssetA.assetType == AztecTypes.AztecAssetType.ETH) { + IERC721(token.collection).transferFrom(address(this), to, token.tokenId); + return (0, 0, false); + } else { + IERC721(token.collection).approve(to, token.tokenId); + NFTVault(to).matchAndPull(_interactionNonce, token.collection, token.tokenId); + return (1, 0, false); + } + } + } + + /** + * @notice Function for the second step of a NFT deposit or for transfers from other NFTVaults. + * @dev For a deposit, this method is called by an Ethereum L1 account that owns the NFT to deposit. + * The user must approve this bridge contract to transfer the users NFT before this function + * is called. This function assumes the NFT contract complies with the ERC721 standard. + * For a transfer from another NFTVault, this method is called by the NFTVault that is sending the NFT. + * + * @param _virtualAssetId - the virutal asset id of the note returned in the deposit step of the convert function + * @param _collection - collection address of the NFT + * @param _tokenId - the token id of the NFT + */ + + function matchAndPull(uint256 _virtualAssetId, address _collection, uint256 _tokenId) external { + if (nftAssets[_virtualAssetId].collection != address(0x0)) { + revert InvalidVirtualAssetId(); + } + nftAssets[_virtualAssetId] = NFTAsset({collection: _collection, tokenId: _tokenId}); + IERC721(_collection).transferFrom(msg.sender, address(this), _tokenId); + emit NFTDeposit(_virtualAssetId, _collection, _tokenId); + } +} diff --git a/src/bridges/registry/AddressRegistry.sol b/src/bridges/registry/AddressRegistry.sol new file mode 100644 index 000000000..cc3b68043 --- /dev/null +++ b/src/bridges/registry/AddressRegistry.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec. +pragma solidity >=0.8.4; + +import {AztecTypes} from "../../../lib/rollup-encoder/src/libraries/AztecTypes.sol"; +import {ErrorLib} from "../base/ErrorLib.sol"; +import {BridgeBase} from "../base/BridgeBase.sol"; + +/** + * @title Aztec Address Registry. + * @author Josh Crites (@critesjosh on Github), Aztec team + * @notice This contract can be used to anonymously register an ethereum address with an id. + * This is useful for reducing the amount of data required to pass an ethereum address through auxData. + * @dev Use this contract to lookup ethereum addresses by id. + */ +contract AddressRegistry is BridgeBase { + uint256 public addressCount; + mapping(uint256 => address) public addresses; + + event AddressRegistered(uint256 indexed index, address indexed entity); + + /** + * @notice Set address of rollup processor + * @param _rollupProcessor Address of rollup processor + */ + constructor(address _rollupProcessor) BridgeBase(_rollupProcessor) {} + + /** + * @notice Function for getting VIRTUAL assets (step 1) to register an address and registering an address (step 2). + * @dev This method can only be called from the RollupProcessor. The first step to register an address is for a user to + * get the type(uint160).max value of VIRTUAL assets back from the bridge. The second step is for the user + * to send an amount of VIRTUAL assets back to the bridge. The amount that is sent back is equal to the number of the + * ethereum address that is being registered (e.g. uint160(0x2e782B05290A7fFfA137a81a2bad2446AD0DdFEB)). + * + * @param _inputAssetA - ETH (step 1) or VIRTUAL (step 2) + * @param _outputAssetA - VIRTUAL (steps 1 and 2) + * @param _totalInputValue - must be 1 wei (ETH) (step 1) or address value (step 2) + * @return outputValueA - type(uint160).max (step 1) or 0 VIRTUAL (step 2) + * + */ + + function convert( + AztecTypes.AztecAsset calldata _inputAssetA, + AztecTypes.AztecAsset calldata, + AztecTypes.AztecAsset calldata _outputAssetA, + AztecTypes.AztecAsset calldata, + uint256 _totalInputValue, + uint256, + uint64, + address + ) external payable override (BridgeBase) onlyRollup returns (uint256 outputValueA, uint256, bool) { + if ( + _inputAssetA.assetType == AztecTypes.AztecAssetType.NOT_USED + || _inputAssetA.assetType == AztecTypes.AztecAssetType.ERC20 + ) revert ErrorLib.InvalidInputA(); + if (_outputAssetA.assetType != AztecTypes.AztecAssetType.VIRTUAL) { + revert ErrorLib.InvalidOutputA(); + } + if (_inputAssetA.assetType == AztecTypes.AztecAssetType.ETH) { + if (_totalInputValue != 1) { + revert ErrorLib.InvalidInputAmount(); + } + return (type(uint160).max, 0, false); + } else if (_inputAssetA.assetType == AztecTypes.AztecAssetType.VIRTUAL) { + address toRegister = address(uint160(_totalInputValue)); + registerAddress(toRegister); + return (0, 0, false); + } + } + + /** + * @notice Register an address at the registry + * @dev This function can be called directly from another Ethereum account. This can be done in + * one step, in one transaction. Coming from Ethereum directly, this method is not as privacy + * preserving as registering an address through the bridge. + * + * @param _to - The address to register + * @return addressCount - the index of address that has been registered + */ + + function registerAddress(address _to) public returns (uint256) { + uint256 userIndex = addressCount++; + addresses[userIndex] = _to; + emit AddressRegistered(userIndex, _to); + return userIndex; + } +} diff --git a/src/deployment/nft-basic/NFTVaultDeployment.s.sol b/src/deployment/nft-basic/NFTVaultDeployment.s.sol new file mode 100644 index 000000000..9d3e7dfe4 --- /dev/null +++ b/src/deployment/nft-basic/NFTVaultDeployment.s.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec. +pragma solidity >=0.8.4; + +import {BaseDeployment} from "../base/BaseDeployment.s.sol"; +import {NFTVault} from "../../bridges/nft-basic/NFTVault.sol"; +import {AddressRegistry} from "../../bridges/registry/AddressRegistry.sol"; + +contract NFTVaultDeployment is BaseDeployment { + function deploy(address _addressRegistry) public returns (address) { + emit log("Deploying NFTVault bridge"); + + vm.broadcast(); + NFTVault bridge = new NFTVault(ROLLUP_PROCESSOR, _addressRegistry); + + emit log_named_address("NFTVault bridge deployed to", address(bridge)); + + return address(bridge); + } + + function deployAndList(address _addressRegistry) public returns (address) { + address bridge = deploy(_addressRegistry); + + uint256 addressId = listBridge(bridge, 135500); + emit log_named_uint("NFTVault bridge address id", addressId); + + return bridge; + } + + function deployAndListAddressRegistry() public returns (address) { + emit log("Deploying AddressRegistry bridge"); + + AddressRegistry bridge = new AddressRegistry(ROLLUP_PROCESSOR); + + emit log_named_address("AddressRegistry bridge deployed to", address(bridge)); + + uint256 addressId = listBridge(address(bridge), 120500); + emit log_named_uint("AddressRegistry bridge address id", addressId); + + return address(bridge); + } +} diff --git a/src/deployment/registry/AddressRegistryDeployment.s.sol b/src/deployment/registry/AddressRegistryDeployment.s.sol new file mode 100644 index 000000000..52cd58b6b --- /dev/null +++ b/src/deployment/registry/AddressRegistryDeployment.s.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec. +pragma solidity >=0.8.4; + +import {BaseDeployment} from "../base/BaseDeployment.s.sol"; +import {AddressRegistry} from "../../bridges/registry/AddressRegistry.sol"; + +contract AddressRegistryDeployment is BaseDeployment { + function deploy() public returns (address) { + emit log("Deploying AddressRegistry bridge"); + + vm.broadcast(); + AddressRegistry bridge = new AddressRegistry(ROLLUP_PROCESSOR); + + emit log_named_address("AddressRegistry bridge deployed to", address(bridge)); + + return address(bridge); + } + + function deployAndList() public returns (address) { + address bridge = deploy(); + + uint256 addressId = listBridge(bridge, 120500); + emit log_named_uint("AddressRegistry bridge address id", addressId); + + return bridge; + } +} diff --git a/src/test/bridges/nft-basic/NFTVaultBasicE2E.t.sol b/src/test/bridges/nft-basic/NFTVaultBasicE2E.t.sol new file mode 100644 index 000000000..6858227e8 --- /dev/null +++ b/src/test/bridges/nft-basic/NFTVaultBasicE2E.t.sol @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec. +pragma solidity >=0.8.4; + +import {BridgeTestBase} from "./../../aztec/base/BridgeTestBase.sol"; +import {AztecTypes} from "rollup-encoder/libraries/AztecTypes.sol"; + +// Example-specific imports +import {NFTVault} from "../../../bridges/nft-basic/NFTVault.sol"; +import {AddressRegistry} from "../../../bridges/registry/AddressRegistry.sol"; +import {ErrorLib} from "../../../bridges/base/ErrorLib.sol"; +import {ERC721PresetMinterPauserAutoId} from + "@openzeppelin/contracts/token/ERC721/presets/ERC721PresetMinterPauserAutoId.sol"; + +/** + * @notice The purpose of this test is to test the bridge in an environment that is as close to the final deployment + * as possible without spinning up all the rollup infrastructure (sequencer, proof generator etc.). + */ +contract NFTVaultBasicE2ETest is BridgeTestBase { + NFTVault internal bridge; + NFTVault internal bridge2; + AddressRegistry private registry; + ERC721PresetMinterPauserAutoId private nftContract; + + // To store the id of the bridge after being added + uint256 private bridgeId; + uint256 private bridge2Id; + uint256 private registryBridgeId; + uint256 private tokenIdToDeposit = 1; + address private constant REGISTER_ADDRESS = 0x2e782B05290A7fFfA137a81a2bad2446AD0DdFEA; + address private constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; + AztecTypes.AztecAsset private ethAsset; + AztecTypes.AztecAsset private virtualAsset1 = + AztecTypes.AztecAsset({id: 1, erc20Address: address(0), assetType: AztecTypes.AztecAssetType.VIRTUAL}); + AztecTypes.AztecAsset private virtualAsset100 = + AztecTypes.AztecAsset({id: 100, erc20Address: address(0), assetType: AztecTypes.AztecAssetType.VIRTUAL}); + AztecTypes.AztecAsset private erc20InputAsset = + AztecTypes.AztecAsset({id: 1, erc20Address: DAI, assetType: AztecTypes.AztecAssetType.ERC20}); + + event NFTDeposit(uint256 indexed virtualAssetId, address indexed collection, uint256 indexed tokenId); + event NFTWithdraw(uint256 indexed virtualAssetId, address indexed collection, uint256 indexed tokenId); + + function setUp() public { + registry = new AddressRegistry(address(ROLLUP_PROCESSOR)); + bridge = new NFTVault(address(ROLLUP_PROCESSOR), address(registry)); + bridge2 = new NFTVault(address(ROLLUP_PROCESSOR), address(registry)); + nftContract = new ERC721PresetMinterPauserAutoId("test", "NFT", ""); + nftContract.mint(address(this)); + nftContract.mint(address(this)); + nftContract.mint(address(this)); + + nftContract.approve(address(bridge), 0); + nftContract.approve(address(bridge), 1); + nftContract.approve(address(bridge), 2); + + ethAsset = ROLLUP_ENCODER.getRealAztecAsset(address(0)); + + vm.label(address(registry), "AddressRegistry Bridge"); + vm.label(address(bridge), "NFTVault Bridge"); + + // Impersonate the multi-sig to add a new bridge + vm.startPrank(MULTI_SIG); + + // WARNING: If you set this value too low the interaction will fail for seemingly no reason! + // OTOH if you se it too high bridge users will pay too much + ROLLUP_PROCESSOR.setSupportedBridge(address(registry), 120500); + ROLLUP_PROCESSOR.setSupportedBridge(address(bridge), 135500); + ROLLUP_PROCESSOR.setSupportedBridge(address(bridge2), 135500); + + vm.stopPrank(); + + // Fetch the id of the bridges + registryBridgeId = ROLLUP_PROCESSOR.getSupportedBridgesLength() - 2; + bridgeId = ROLLUP_PROCESSOR.getSupportedBridgesLength() - 1; + bridge2Id = ROLLUP_PROCESSOR.getSupportedBridgesLength(); + // get virtual assets to register an address + ROLLUP_ENCODER.defiInteractionL2(registryBridgeId, ethAsset, emptyAsset, virtualAsset1, emptyAsset, 0, 1); + ROLLUP_ENCODER.processRollup(); + // get virtual assets to register 2nd NFTVault + ROLLUP_ENCODER.defiInteractionL2(registryBridgeId, ethAsset, emptyAsset, virtualAsset1, emptyAsset, 0, 1); + ROLLUP_ENCODER.processRollup(); + + // register an address + uint160 inputAmount = uint160(REGISTER_ADDRESS); + ROLLUP_ENCODER.defiInteractionL2( + registryBridgeId, virtualAsset1, emptyAsset, virtualAsset1, emptyAsset, 0, inputAmount + ); + ROLLUP_ENCODER.processRollup(); + + // register 2nd NFTVault in AddressRegistry + uint160 bridge2AddressAmount = uint160(address(bridge2)); + ROLLUP_ENCODER.defiInteractionL2( + registryBridgeId, virtualAsset1, emptyAsset, virtualAsset1, emptyAsset, 0, bridge2AddressAmount + ); + } + + function testDeposit() public { + // get virtual asset before deposit + ROLLUP_ENCODER.defiInteractionL2(bridgeId, ethAsset, emptyAsset, virtualAsset100, emptyAsset, 0, 1); + + (uint256 outputValueA, uint256 outputValueB, bool isAsync) = ROLLUP_ENCODER.processRollupAndGetBridgeResult(); + + assertEq(outputValueA, 1, "Output value A doesn't equal 1"); + assertEq(outputValueB, 0, "Output value B is not 0"); + assertTrue(!isAsync, "Bridge is incorrectly in an async mode"); + + address collection = address(nftContract); + + vm.expectEmit(true, true, true, false); + emit NFTDeposit(virtualAsset100.id, collection, tokenIdToDeposit); + bridge.matchAndPull(virtualAsset100.id, collection, tokenIdToDeposit); + (address returnedCollection, uint256 returnedId) = bridge.nftAssets(virtualAsset100.id); + assertEq(returnedId, tokenIdToDeposit, "nft token id does not match input"); + assertEq(returnedCollection, collection, "collection data does not match"); + } + + function testWithdraw() public { + testDeposit(); + uint64 auxData = uint64(registry.addressCount() - 2); + + vm.expectEmit(true, true, false, false); + emit NFTWithdraw(virtualAsset100.id, address(nftContract), tokenIdToDeposit); + ROLLUP_ENCODER.defiInteractionL2(bridgeId, virtualAsset100, emptyAsset, ethAsset, emptyAsset, auxData, 1); + + (uint256 outputValueA, uint256 outputValueB, bool isAsync) = ROLLUP_ENCODER.processRollupAndGetBridgeResult(); + address owner = nftContract.ownerOf(tokenIdToDeposit); + assertEq(REGISTER_ADDRESS, owner, "registered address is not the owner"); + assertEq(outputValueA, 0, "Output value A is not 0"); + assertEq(outputValueB, 0, "Output value B is not 0"); + assertTrue(!isAsync, "Bridge is incorrectly in an async mode"); + + (address _a, uint256 _id) = bridge.nftAssets(virtualAsset100.id); + assertEq(_a, address(0), "collection address is not 0"); + assertEq(_id, 0, "token id is not 0"); + } + + function testTransfer() public { + testDeposit(); + (address collection, uint256 tokenId) = bridge.nftAssets(virtualAsset100.id); + uint64 auxData = uint64(registry.addressCount() - 1); + + uint256 interactionNonce = ROLLUP_ENCODER.getNextNonce(); + + vm.expectEmit(true, true, true, false, address(bridge)); + emit NFTWithdraw(virtualAsset100.id, collection, tokenId); + vm.expectEmit(true, true, true, false, address(bridge2)); + emit NFTDeposit(interactionNonce, collection, tokenId); + ROLLUP_ENCODER.defiInteractionL2(bridgeId, virtualAsset100, emptyAsset, virtualAsset1, emptyAsset, auxData, 1); + + ROLLUP_ENCODER.processRollup(); + + // check that the nft was transferred to the second NFTVault + (address returnedCollection, uint256 returnedId) = bridge2.nftAssets(interactionNonce); + assertEq(returnedId, tokenIdToDeposit, "nft token id does not match input"); + assertEq(returnedCollection, collection, "collection data does not match"); + } +} diff --git a/src/test/bridges/nft-basic/NFTVaultBasicUnit.t.sol b/src/test/bridges/nft-basic/NFTVaultBasicUnit.t.sol new file mode 100644 index 000000000..7fdf5ce43 --- /dev/null +++ b/src/test/bridges/nft-basic/NFTVaultBasicUnit.t.sol @@ -0,0 +1,257 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec. +pragma solidity >=0.8.4; + +import {BridgeTestBase} from "./../../aztec/base/BridgeTestBase.sol"; +import {AztecTypes} from "rollup-encoder/libraries/AztecTypes.sol"; + +// Example-specific imports +import {ERC721PresetMinterPauserAutoId} from + "@openzeppelin/contracts/token/ERC721/presets/ERC721PresetMinterPauserAutoId.sol"; +import {NFTVault} from "../../../bridges/nft-basic/NFTVault.sol"; +import {ErrorLib} from "../../../bridges/base/ErrorLib.sol"; +import {AddressRegistry} from "../../../bridges/registry/AddressRegistry.sol"; + +// @notice The purpose of this test is to directly test convert functionality of the bridge. +contract NFTVaultBasicUnitTest is BridgeTestBase { + struct NftAsset { + address collection; + uint256 id; + } + + address private rollupProcessor; + + NFTVault private bridge; + NFTVault private bridge2; + ERC721PresetMinterPauserAutoId private nftContract; + uint256 private tokenIdToDeposit = 1; + AddressRegistry private registry; + address private constant REGISTER_ADDRESS = 0x2e782B05290A7fFfA137a81a2bad2446AD0DdFEA; + address private constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; + + AztecTypes.AztecAsset private ethAsset = + AztecTypes.AztecAsset({id: 0, erc20Address: address(0), assetType: AztecTypes.AztecAssetType.ETH}); + AztecTypes.AztecAsset private virtualAsset1 = + AztecTypes.AztecAsset({id: 1, erc20Address: address(0), assetType: AztecTypes.AztecAssetType.VIRTUAL}); + AztecTypes.AztecAsset private virtualAsset100 = + AztecTypes.AztecAsset({id: 100, erc20Address: address(0), assetType: AztecTypes.AztecAssetType.VIRTUAL}); + AztecTypes.AztecAsset private erc20InputAsset = + AztecTypes.AztecAsset({id: 1, erc20Address: DAI, assetType: AztecTypes.AztecAssetType.ERC20}); + + // @dev This method exists on RollupProcessor.sol. It's defined here in order to be able to receive ETH like a real + // rollup processor would. + function receiveEthFromBridge(uint256 _interactionNonce) external payable {} + + function setUp() public { + // In unit tests we set address of rollupProcessor to the address of this test contract + rollupProcessor = address(this); + + registry = new AddressRegistry(rollupProcessor); + bridge = new NFTVault(rollupProcessor, address(registry)); + bridge2 = new NFTVault(rollupProcessor, address(registry)); + nftContract = new ERC721PresetMinterPauserAutoId("test", "NFT", ""); + nftContract.mint(address(this)); + nftContract.mint(address(this)); + nftContract.mint(address(this)); + + nftContract.approve(address(bridge), 0); + nftContract.approve(address(bridge), 1); + nftContract.approve(address(bridge), 2); + + _registerAddress(REGISTER_ADDRESS); + _registerAddress(address(bridge2)); + + // Set ETH balance of bridge to 0 for clarity (somebody sent ETH to that address on mainnet) + vm.deal(address(bridge), 0); + vm.label(address(bridge), "Basic NFT Vault Bridge"); + } + + function testInvalidCaller(address _callerAddress) public { + vm.assume(_callerAddress != rollupProcessor); + // Use HEVM cheatcode to call from a different address than is address(this) + vm.prank(_callerAddress); + vm.expectRevert(ErrorLib.InvalidCaller.selector); + bridge.convert(emptyAsset, emptyAsset, emptyAsset, emptyAsset, 0, 0, 0, address(0)); + } + + function testInvalidInputAssetType() public { + vm.expectRevert(ErrorLib.InvalidInputA.selector); + bridge.convert(emptyAsset, emptyAsset, emptyAsset, emptyAsset, 0, 0, 0, address(0)); + } + + function testInvalidOutputAssetType() public { + vm.expectRevert(ErrorLib.InvalidOutputA.selector); + bridge.convert(ethAsset, emptyAsset, erc20InputAsset, emptyAsset, 0, 0, 0, address(0)); + } + + function testGetVirtualAssetUnitTest() public { + vm.warp(block.timestamp + 1 days); + + (uint256 outputValueA, uint256 outputValueB, bool isAsync) = bridge.convert( + ethAsset, // _inputAssetA + emptyAsset, // _inputAssetB + virtualAsset100, // _outputAssetA + emptyAsset, // _outputAssetB + 1, // _totalInputValue + 0, // _interactionNonce + 0, // _auxData + address(0) // _rollupBeneficiary + ); + + assertEq(outputValueA, 1, "Output value A doesn't equal 1"); + assertEq(outputValueB, 0, "Output value B is not 0"); + assertTrue(!isAsync, "Bridge is incorrectly in an async mode"); + } + + // should fail because sending more than 1 wei + function testGetVirtualAssetShouldFail() public { + vm.warp(block.timestamp + 1 days); + + vm.expectRevert(); + bridge.convert( + ethAsset, // _inputAssetA + emptyAsset, // _inputAssetB + virtualAsset100, // _outputAssetA + emptyAsset, // _outputAssetB + 2, // _totalInputValue + 0, // _interactionNonce + 0, // _auxData + address(0) // _rollupBeneficiary + ); + } + + function testDeposit() public { + vm.warp(block.timestamp + 1 days); + + address collection = address(nftContract); + bridge.matchAndPull(virtualAsset100.id, collection, tokenIdToDeposit); + (address returnedCollection, uint256 returnedId) = bridge.nftAssets(virtualAsset100.id); + assertEq(returnedId, tokenIdToDeposit, "nft token id does not match input"); + assertEq(returnedCollection, collection, "collection data does not match"); + } + + // should fail because an NFT with this id has already been deposited + function testDepositFailWithDuplicateNft() public { + testDeposit(); + vm.warp(block.timestamp + 1 days); + + address collection = address(nftContract); + vm.expectRevert(); + bridge.matchAndPull(virtualAsset100.id, collection, tokenIdToDeposit); + } + + // should fail because no withdraw address has been registered with this id + function testWithdrawUnregisteredWithdrawAddress() public { + testDeposit(); + uint64 auxData = 1000; + vm.expectRevert(ErrorLib.InvalidAuxData.selector); + bridge.convert( + virtualAsset100, // _inputAssetA + emptyAsset, // _inputAssetB + ethAsset, // _outputAssetA + emptyAsset, // _outputAssetB + 1, // _totalInputValue + 0, // _interactionNonce + auxData, + address(0) + ); + } + + function testWithdraw() public { + testDeposit(); + uint64 auxData = uint64(registry.addressCount() - 2); + (uint256 outputValueA, uint256 outputValueB, bool isAsync) = bridge.convert( + virtualAsset100, // _inputAssetA + emptyAsset, // _inputAssetB + ethAsset, // _outputAssetA + emptyAsset, // _outputAssetB + 1, // _totalInputValue + 0, // _interactionNonce + auxData, // _auxData + address(0) + ); + address owner = nftContract.ownerOf(tokenIdToDeposit); + assertEq(REGISTER_ADDRESS, owner, "registered address is not the owner"); + assertEq(outputValueA, 0, "Output value A is not 0"); + assertEq(outputValueB, 0, "Output value B is not 0"); + assertTrue(!isAsync, "Bridge is incorrectly in an async mode"); + + (address _a, uint256 _id) = bridge.nftAssets(virtualAsset100.id); + assertEq(_a, address(0), "collection address is not 0"); + assertEq(_id, 0, "token id is not 0"); + } + + // should fail because no NFT has been registered with this virtual asset + function testWithdrawUnregisteredNft() public { + testDeposit(); + uint64 auxData = uint64(registry.addressCount()); + vm.expectRevert(ErrorLib.InvalidInputA.selector); + bridge.convert( + virtualAsset1, // _inputAssetA + emptyAsset, // _inputAssetB + ethAsset, // _outputAssetA + emptyAsset, // _outputAssetB + 1, // _totalInputValue + 0, // _interactionNonce + auxData, + address(0) + ); + } + + function testTransfer() public { + testDeposit(); + uint64 auxData = uint64(registry.addressCount() - 1); + uint256 interactionNonce = 128; + + (uint256 outputValueA, uint256 outputValueB, bool isAsync) = bridge.convert( + virtualAsset100, // _inputAssetA + emptyAsset, // _inputAssetB + virtualAsset100, // _outputAssetA + emptyAsset, // _outputAssetB + 1, // _totalInputValue + interactionNonce, // _interactionNonce + auxData, // _auxData + address(0) + ); + address owner = nftContract.ownerOf(tokenIdToDeposit); + assertEq(address(bridge2), owner, "registered address is not the owner"); + assertEq(outputValueA, 1, "Output value A is not 1"); + assertEq(outputValueB, 0, "Output value B is not 0"); + assertTrue(!isAsync, "Bridge is incorrectly in an async mode"); + + // test that the nft was deleted from bridge 1 + (address bridge1collection,) = bridge.nftAssets(virtualAsset100.id); + assertEq(bridge1collection, address(0), "collection was not deleted"); + + // test that the nft was added to bridge 2 + (address _a, uint256 _id) = bridge2.nftAssets(interactionNonce); + assertEq(_a, address(nftContract), "collection address is not 0"); + assertEq(_id, tokenIdToDeposit, "token id is not 0"); + } + + function _registerAddress(address _addressToRegister) internal { + // get virtual assets + registry.convert( + ethAsset, + emptyAsset, + virtualAsset1, + emptyAsset, + 1, // _totalInputValue + 0, // _interactionNonce + 0, // _auxData + address(0x0) + ); + uint256 inputAmount = uint160(address(_addressToRegister)); + // register an address + registry.convert( + virtualAsset1, + emptyAsset, + virtualAsset1, + emptyAsset, + inputAmount, + 0, // _interactionNonce + 0, // _auxData + address(0x0) + ); + } +} diff --git a/src/test/bridges/registry/AddressRegistryE2E.t.sol b/src/test/bridges/registry/AddressRegistryE2E.t.sol new file mode 100644 index 000000000..2191ac198 --- /dev/null +++ b/src/test/bridges/registry/AddressRegistryE2E.t.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec. +pragma solidity >=0.8.4; + +import {BridgeTestBase} from "./../../aztec/base/BridgeTestBase.sol"; +import {AztecTypes} from "../../../../lib/rollup-encoder/src/libraries/AztecTypes.sol"; + +// Example-specific imports +import {AddressRegistry} from "../../../bridges/registry/AddressRegistry.sol"; +import {ErrorLib} from "../../../bridges/base/ErrorLib.sol"; + +/** + * @notice The purpose of this test is to test the bridge in an environment that is as close to the final deployment + * as possible without spinning up all the rollup infrastructure (sequencer, proof generator etc.). + */ +contract AddressRegistryE2ETest is BridgeTestBase { + AddressRegistry internal bridge; + uint256 private id; + AztecTypes.AztecAsset private ethAsset; + AztecTypes.AztecAsset private virtualAsset1; + uint256 public maxInt = type(uint160).max; + + event AddressRegistered(uint256 indexed addressCount, address indexed registeredAddress); + + function setUp() public { + bridge = new AddressRegistry(address(ROLLUP_PROCESSOR)); + ethAsset = ROLLUP_ENCODER.getRealAztecAsset(address(0)); + virtualAsset1 = + AztecTypes.AztecAsset({id: 0, erc20Address: address(0), assetType: AztecTypes.AztecAssetType.VIRTUAL}); + + vm.label(address(bridge), "Address Registry Bridge"); + + // Impersonate the multi-sig to add a new bridge + vm.startPrank(MULTI_SIG); + + // WARNING: If you set this value too low the interaction will fail for seemingly no reason! + // OTOH if you se it too high bridge users will pay too much + ROLLUP_PROCESSOR.setSupportedBridge(address(bridge), 120000); + + vm.stopPrank(); + + // Fetch the id of the example bridge + id = ROLLUP_PROCESSOR.getSupportedBridgesLength(); + } + + function testGetVirtualAssets() public { + vm.warp(block.timestamp + 1 days); + + ROLLUP_ENCODER.defiInteractionL2(id, ethAsset, emptyAsset, virtualAsset1, emptyAsset, 0, 1); + + (uint256 outputValueA, uint256 outputValueB, bool isAsync) = ROLLUP_ENCODER.processRollupAndGetBridgeResult(); + + assertEq(outputValueA, maxInt, "outputValueA doesn't equal maxInt"); + assertEq(outputValueB, 0, "Non-zero outputValueB"); + assertFalse(isAsync, "Bridge is not synchronous"); + } + + function testRegistration() public { + uint160 inputAmount = uint160(0x2e782B05290A7fFfA137a81a2bad2446AD0DdFEA); + + vm.expectEmit(true, true, false, false); + emit AddressRegistered(0, address(inputAmount)); + + ROLLUP_ENCODER.defiInteractionL2(id, virtualAsset1, emptyAsset, virtualAsset1, emptyAsset, 0, inputAmount); + + (uint256 outputValueA, uint256 outputValueB, bool isAsync) = ROLLUP_ENCODER.processRollupAndGetBridgeResult(); + + uint64 addressId = uint64(bridge.addressCount()) - 1; + address newlyRegistered = bridge.addresses(addressId); + + assertEq(address(inputAmount), newlyRegistered, "input amount doesn't equal newly registered address"); + assertEq(outputValueA, 0, "Non-zero outputValueA"); + assertEq(outputValueB, 0, "Non-zero outputValueB"); + assertFalse(isAsync, "Bridge is not synchronous"); + } +} diff --git a/src/test/bridges/registry/AddressRegistryUnitTest.t.sol b/src/test/bridges/registry/AddressRegistryUnitTest.t.sol new file mode 100644 index 000000000..a66fcd274 --- /dev/null +++ b/src/test/bridges/registry/AddressRegistryUnitTest.t.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec. +pragma solidity >=0.8.4; + +import {BridgeTestBase} from "./../../aztec/base/BridgeTestBase.sol"; +import {AztecTypes} from "rollup-encoder/libraries/AztecTypes.sol"; + +// Example-specific imports +import {AddressRegistry} from "../../../bridges/registry/AddressRegistry.sol"; +import {ErrorLib} from "../../../bridges/base/ErrorLib.sol"; + +// @notice The purpose of this test is to directly test convert functionality of the bridge. +contract AddressRegistryUnitTest is BridgeTestBase { + address private constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; + address private rollupProcessor; + AddressRegistry private bridge; + uint256 public maxInt = type(uint160).max; + AztecTypes.AztecAsset private ethAsset = + AztecTypes.AztecAsset({id: 0, erc20Address: address(0), assetType: AztecTypes.AztecAssetType.ETH}); + AztecTypes.AztecAsset private virtualAsset = + AztecTypes.AztecAsset({id: 0, erc20Address: address(0), assetType: AztecTypes.AztecAssetType.VIRTUAL}); + AztecTypes.AztecAsset private daiAsset = + AztecTypes.AztecAsset({id: 1, erc20Address: DAI, assetType: AztecTypes.AztecAssetType.ERC20}); + + event AddressRegistered(uint256 indexed addressCount, address indexed registeredAddress); + + // @dev This method exists on RollupProcessor.sol. It's defined here in order to be able to receive ETH like a real + // rollup processor would. + function receiveEthFromBridge(uint256 _interactionNonce) external payable {} + + function setUp() public { + // In unit tests we set address of rollupProcessor to the address of this test contract + rollupProcessor = address(this); + + bridge = new AddressRegistry(rollupProcessor); + + // Use the label cheatcode to mark the address with "AddressRegistry Bridge" in the traces + vm.label(address(bridge), "AddressRegistry Bridge"); + } + + function testInvalidCaller(address _callerAddress) public { + vm.assume(_callerAddress != rollupProcessor); + // Use HEVM cheatcode to call from a different address than is address(this) + vm.prank(_callerAddress); + vm.expectRevert(ErrorLib.InvalidCaller.selector); + bridge.convert(emptyAsset, emptyAsset, emptyAsset, emptyAsset, 0, 0, 0, address(0)); + } + + function testInvalidInputAssetType() public { + vm.expectRevert(ErrorLib.InvalidInputA.selector); + bridge.convert(daiAsset, emptyAsset, emptyAsset, emptyAsset, 0, 0, 0, address(0)); + } + + function testInvalidOutputAssetType() public { + vm.expectRevert(ErrorLib.InvalidOutputA.selector); + bridge.convert(ethAsset, emptyAsset, daiAsset, emptyAsset, 0, 0, 0, address(0)); + } + + function testInvalidInputAmount() public { + vm.expectRevert(ErrorLib.InvalidInputAmount.selector); + + bridge.convert( + ethAsset, + emptyAsset, + virtualAsset, + emptyAsset, + 0, // _totalInputValue + 0, // _interactionNonce + 0, // _auxData + address(0x0) + ); + } + + function testGetBackMaxVirtualAssets() public { + vm.warp(block.timestamp + 1 days); + + (uint256 outputValueA, uint256 outputValueB, bool isAsync) = bridge.convert( + ethAsset, + emptyAsset, + virtualAsset, + emptyAsset, + 1, // _totalInputValue + 0, // _interactionNonce + 0, // _auxData + address(0x0) + ); + + assertEq(outputValueA, maxInt, "Output value A doesn't equal maxInt"); + assertEq(outputValueB, 0, "Output value B is not 0"); + assertTrue(!isAsync, "Bridge is incorrectly in an async mode"); + } + + function testRegistringAnAddress() public { + vm.warp(block.timestamp + 1 days); + + uint160 inputAmount = uint160(0x2e782B05290A7fFfA137a81a2bad2446AD0DdFEA); + + vm.expectEmit(true, true, false, false); + emit AddressRegistered(0, address(inputAmount)); + + (uint256 outputValueA, uint256 outputValueB, bool isAsync) = bridge.convert( + virtualAsset, + emptyAsset, + virtualAsset, + emptyAsset, + inputAmount, // _totalInputValue + 0, // _interactionNonce + 0, // _auxData + address(0x0) + ); + + uint256 id = bridge.addressCount() - 1; + address newlyRegistered = bridge.addresses(id); + + assertEq(address(inputAmount), newlyRegistered, "Address not registered"); + assertEq(outputValueA, 0, "Output value is not 0"); + assertEq(outputValueB, 0, "Output value B is not 0"); + assertTrue(!isAsync, "Bridge is incorrectly in an async mode"); + } + + function testRegisterFromEth() public { + address to = address(0x2e782B05290A7fFfA137a81a2bad2446AD0DdFEA); + uint256 count = bridge.registerAddress(to); + address registered = bridge.addresses(count); + assertEq(to, registered, "Address not registered"); + } +}