diff --git a/.gitignore b/.gitignore index 4b6e85fbc..b8da22489 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/ +.env diff --git a/lib/forge-std b/lib/forge-std index d666309ed..b8a11f5ba 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit d666309ed272e7fa16fa35f28d63ee6442df45fc +Subproject commit b8a11f5ba2eaac60949324c1f50b2f748f38b4be diff --git a/lib/rollup-encoder b/lib/rollup-encoder index c3ce0d8c6..64b9c1183 160000 --- a/lib/rollup-encoder +++ b/lib/rollup-encoder @@ -1 +1 @@ -Subproject commit c3ce0d8c691bb6a9361156e34e8e693608a32698 +Subproject commit 64b9c1183b76b30006f9954cccf2d39055c72644 diff --git a/src/bridges/nft-basic/NFTVault.sol b/src/bridges/nft-basic/NFTVault.sol new file mode 100644 index 000000000..d37ddbe28 --- /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/registry/AddressRegistry.sol b/src/bridges/registry/AddressRegistry.sol new file mode 100644 index 000000000..d7bad34fc --- /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/gas/nft-basic/NFTVaultGas.s.sol b/src/gas/nft-basic/NFTVaultGas.s.sol new file mode 100644 index 000000000..ae0593ed3 --- /dev/null +++ b/src/gas/nft-basic/NFTVaultGas.s.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec. +pragma solidity >=0.8.4; + +import {AddressRegistry} from "../../bridges/registry/AddressRegistry.sol"; +import {AddressRegistryDeployment} from "../../deployment/registry/AddressRegistryDeployment.s.sol"; +import {NFTVault} from "../../bridges/nft-basic/NFTVault.sol"; +import {AztecTypes} from "rollup-encoder/libraries/AztecTypes.sol"; + +import {NFTVaultDeployment} from "../../deployment/nft-basic/NFTVaultDeployment.s.sol"; +import {GasBase} from "../base/GasBase.sol"; + +import {ERC721PresetMinterPauserAutoId} from + "@openzeppelin/contracts/token/ERC721/presets/ERC721PresetMinterPauserAutoId.sol"; + +interface IRead { + function defiBridgeProxy() external view returns (address); +} + +contract NFTVaultGas is NFTVaultDeployment { + GasBase internal gasBase; + NFTVault internal bridge; + NFTVault internal bridge2; + ERC721PresetMinterPauserAutoId internal nftContract; + address internal registry; + uint256 internal registryAddressId; + + AztecTypes.AztecAsset private empty; + AztecTypes.AztecAsset private eth = + 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 virtualAsset128 = + AztecTypes.AztecAsset({id: 128, erc20Address: address(0), assetType: AztecTypes.AztecAssetType.VIRTUAL}); + + function measure() public { + uint256 privKey1 = vm.envUint("PRIVATE_KEY"); + address addr1 = vm.addr(privKey1); + + address defiProxy = IRead(ROLLUP_PROCESSOR).defiBridgeProxy(); + vm.label(defiProxy, "DefiProxy"); + + vm.startBroadcast(); + gasBase = new GasBase(defiProxy); + nftContract = new ERC721PresetMinterPauserAutoId("test", "NFT", ""); + nftContract.mint(addr1); + vm.stopBroadcast(); + + address temp = ROLLUP_PROCESSOR; + ROLLUP_PROCESSOR = address(gasBase); + registry = deployAndListAddressRegistry(); + address bridge = deployAndList(registry); + address bridge2 = deployAndList(registry); + ROLLUP_PROCESSOR = temp; + + vm.startBroadcast(); + address(gasBase).call{value: 4 ether}(""); + + _registerAddress(addr1); + _registerAddress(bridge); + _registerAddress(bridge2); + + nftContract.approve(address(bridge), 0); + vm.stopBroadcast(); + + // Get virtual assets + { + vm.broadcast(); + gasBase.convert(bridge, eth, empty, virtualAsset, empty, 1, 0, 0, address(0), 400000); + } + // deposit nft + { + vm.broadcast(); + NFTVault(bridge).matchAndPull(virtualAsset.id, address(nftContract), 0); + } + // transfer nft + { + vm.broadcast(); + gasBase.convert(bridge, virtualAsset, empty, virtualAsset128, empty, 1, 128, 2, address(0), 4000000); + } + // withdraw nft + { + vm.broadcast(); + gasBase.convert(bridge2, virtualAsset128, empty, eth, empty, 1, 0, 0, address(0), 400000); + } + } + + function _registerAddress(address _toRegister) internal { + // get registry virtual asset + gasBase.convert(address(registry), eth, empty, virtualAsset, empty, 1, 0, 0, address(0), 4000000); + // register address + gasBase.convert( + address(registry), + virtualAsset, + empty, + virtualAsset, + empty, + uint256(uint160(_toRegister)), + 0, + 0, + address(0), + 4000000 + ); + } +} diff --git a/src/gas/nft-basic/README.md b/src/gas/nft-basic/README.md new file mode 100644 index 000000000..44564d1c7 --- /dev/null +++ b/src/gas/nft-basic/README.md @@ -0,0 +1,16 @@ +# how to run this + +1. create an .env file +``` +PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 +network=mainnet +simulateAdmin=false +``` +2. start anvil +``` +anvil --fork-url https://mainnet.infura.io/v3/${API_KEY} +``` +3. run this command +``` +forge script src/gas/nft-basic/NFTVaultGas.s.sol --fork-url http://localhost:8545 --sig "measure()" --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 -vvvv +``` \ No newline at end of file diff --git a/src/gas/registry/AddressRegistryGas.s.sol b/src/gas/registry/AddressRegistryGas.s.sol new file mode 100644 index 000000000..b452fd286 --- /dev/null +++ b/src/gas/registry/AddressRegistryGas.s.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec. +pragma solidity >=0.8.4; + +import {AddressRegistry} from "../../bridges/registry/AddressRegistry.sol"; +import {AztecTypes} from "rollup-encoder/libraries/AztecTypes.sol"; + +import {AddressRegistryDeployment} from "../../deployment/registry/AddressRegistryDeployment.s.sol"; +import {GasBase} from "../base/GasBase.sol"; + +interface IRead { + function defiBridgeProxy() external view returns (address); +} + +contract AddressRegistryGas is AddressRegistryDeployment { + GasBase internal gasBase; + AddressRegistry internal bridge; + + function measure() public { + address defiProxy = IRead(ROLLUP_PROCESSOR).defiBridgeProxy(); + vm.label(defiProxy, "DefiProxy"); + + vm.broadcast(); + gasBase = new GasBase(defiProxy); + + address temp = ROLLUP_PROCESSOR; + ROLLUP_PROCESSOR = address(gasBase); + address bridge = deployAndList(); + ROLLUP_PROCESSOR = temp; + + AztecTypes.AztecAsset memory empty; + AztecTypes.AztecAsset memory eth = + AztecTypes.AztecAsset({id: 0, erc20Address: address(0), assetType: AztecTypes.AztecAssetType.ETH}); + AztecTypes.AztecAsset memory virtualAsset = + AztecTypes.AztecAsset({id: 100, erc20Address: address(0), assetType: AztecTypes.AztecAssetType.VIRTUAL}); + + vm.broadcast(); + address(gasBase).call{value: 2 ether}(""); + emit log_named_uint("Balance of ", address(gasBase).balance); + + // Get virtual assets + { + vm.broadcast(); + gasBase.convert(bridge, eth, empty, virtualAsset, empty, 1, 0, 0, address(0), 400000); + } + + uint256 inputAmount = uint256(uint160(0x2e782B05290A7fFfA137a81a2bad2446AD0DdFEA)); + // register address + { + vm.broadcast(); + gasBase.convert(bridge, virtualAsset, empty, virtualAsset, empty, inputAmount, 0, 0, address(0), 400000); + } + } +} 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"); + } +}