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/specs/bridges/nft_trading/readme.md b/specs/bridges/nft_trading/readme.md new file mode 100644 index 000000000..85d463c89 --- /dev/null +++ b/specs/bridges/nft_trading/readme.md @@ -0,0 +1,54 @@ +# 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 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 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` to pull tokens from it. + + +## 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 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/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(); + } +} 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/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"); + } +} 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"); + } +}