diff --git a/nest/src/interfaces/IBoringVaultAdapter.sol b/nest/src/interfaces/IBoringVaultAdapter.sol index b227676..e6391d4 100644 --- a/nest/src/interfaces/IBoringVaultAdapter.sol +++ b/nest/src/interfaces/IBoringVaultAdapter.sol @@ -5,6 +5,31 @@ import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol"; interface IBoringVaultAdapter { + // LayerZero functions + function setTrustedRemote(uint16 _remoteChainId, bytes calldata _path) external; + function getTrustedRemote( + uint16 _remoteChainId + ) external view returns (bytes memory); + function setMinDstGas(uint16 _dstChainId, uint16 _packetType, uint256 _minGas) external; + function setConfig(uint16 _version, uint16 _chainId, uint256 _configType, bytes calldata _config) external; + function setSendVersion( + uint16 _version + ) external; + function setReceiveVersion( + uint16 _version + ) external; + function forceResumeReceive(uint16 _srcChainId, bytes calldata _srcAddress) external; + + // OFT functions + function sendFrom( + address _from, + uint16 _dstChainId, + bytes32 _toAddress, + uint256 _amount, + address payable _refundAddress, + bytes calldata _payload + ) external payable; + // View Functions function getVault() external view returns (address); function getTeller() external view returns (address); @@ -57,4 +82,8 @@ interface IBoringVaultAdapter { event VaultChanged(address oldVault, address newVault); event Reinitialized(uint256 version); + // LZ Events + event SetTrustedRemote(uint16 _remoteChainId, bytes _path); + event SetMinDstGas(uint16 _dstChainId, uint16 _packetType, uint256 _minGas); + } diff --git a/nest/test/pUSD_LZTest.t.sol b/nest/test/pUSD_LZTest.t.sol index 5317b8b..fe95f09 100644 --- a/nest/test/pUSD_LZTest.t.sol +++ b/nest/test/pUSD_LZTest.t.sol @@ -15,10 +15,18 @@ import { MockTeller } from "../src/mocks/MockTeller.sol"; import { IBoringVault } from "../src/interfaces/IBoringVault.sol"; import { IBoringVaultAdapter } from "../src/interfaces/IBoringVaultAdapter.sol"; +import { BoringVaultAdapter } from "../src/token/BoringVaultAdapter.sol"; + import { MockUSDC } from "../src/mocks/MockUSDC.sol"; import { MockVault } from "../src/mocks/MockVault.sol"; import { pUSD } from "../src/token/pUSD.sol"; +import { OFTCore } from "@layerzerolabs/oft-evm/contracts/OFTCore.sol"; +import { SendParam, MessagingFee } from "@layerzerolabs/oft-evm/contracts/interfaces/IOFT.sol"; +import { ILayerZeroEndpointV2 } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; +import { SetConfigParam } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/IMessageLibManager.sol"; + + interface BoringVault { function enter(address from, IERC20 asset, uint256 assetAmount, address to, uint256 shareAmount) external; @@ -57,63 +65,166 @@ contract pUSD_LZTest is Test { string PLUME_MAINNET_RPC = vm.envString("PLUME_MAINNET_RPC"); string ETH_MAINNET_RPC = vm.envString("ETH_MAINNET_RPC"); - uint256 plumeMainnetFork; uint256 ethMainnetFork; + uint256 plumeMainnetFork; - // Test contracts - pUSD public ethImplementation; - pUSD public plumeImplementation; - pUSD public ethProxy; - pUSD public plumeProxy; - - // Test accounts - address public owner; - address public user; - - // Create mock addresses for ETH mainnet deployment - // Mock contracts for ETH mainnet - MockVault public mockVault; - MockTeller public mockTeller; - MockAtomicQueue public mockQueue; - MockLens public mockLens; - MockAccountantWithRateProviders public mockAccountant; - MockUSDC public mockUSDC; - - IBoringVault vault; - IBoringVaultAdapter adapter; - - function setUp() public { - // Create forks - ethMainnetFork = vm.createFork(vm.envString("ETH_MAINNET_RPC")); - plumeMainnetFork = vm.createFork(vm.envString("PLUME_MAINNET_RPC")); + address owner; + address user; + + pUSD ethImplementation; + pUSD plumeImplementation; + pUSD ethProxy; + pUSD plumeProxy; + + // Interfaces for vault and OFT functionality + IBoringVaultAdapter ethVault; + IBoringVaultAdapter plumeVault; + OFTCore ethOFT; + OFTCore plumeOFT; + +function setUp() public { + // Create forks + ethMainnetFork = vm.createFork(vm.envString("ETH_MAINNET_RPC")); + plumeMainnetFork = vm.createFork(vm.envString("PLUME_MAINNET_RPC")); + + // Create users + owner = makeAddr("owner"); + user = makeAddr("user"); + + // Deploy on ETH mainnet + vm.selectFork(ethMainnetFork); + ethImplementation = new pUSD( + ETH_LZ_ENDPOINT, + ETH_LZ_DELEGATE, + owner + ); + vm.makePersistent(address(ethImplementation)); + + // Deploy and initialize ETH proxy + ERC1967Proxy ethProxyContract = new ERC1967Proxy( + address(ethImplementation), + abi.encodeCall( + pUSD.initialize, + ( + owner, + IERC20(ETH_USDC), + VAULT_TOKEN, + TELLER_ADDRESS, + ATOMIC_QUEUE, + LENS_ADDRESS, + ACCOUNTANT_ADDRESS, + ETH_LZ_ENDPOINT, + ETH_EID + ) + ) + ); + ethProxy = pUSD(address(ethProxyContract)); + + // Make ETH contracts and endpoints persistent on ETH fork + vm.makePersistent(address(ethProxy)); + vm.makePersistent(address(ethProxyContract)); + vm.makePersistent(ETH_LZ_ENDPOINT); + + // Deploy on Plume + vm.selectFork(plumeMainnetFork); + plumeImplementation = new pUSD( + PLUME_LZ_ENDPOINT, + PLUME_LZ_DELEGATE, + owner + ); + vm.makePersistent(address(plumeImplementation)); + + // Deploy and initialize Plume proxy + ERC1967Proxy plumeProxyContract = new ERC1967Proxy( + address(plumeImplementation), + abi.encodeCall( + pUSD.initialize, + ( + owner, + IERC20(PLUME_USDC), + VAULT_TOKEN, + TELLER_ADDRESS, + ATOMIC_QUEUE, + LENS_ADDRESS, + ACCOUNTANT_ADDRESS, + PLUME_LZ_ENDPOINT, + PLUME_EID + ) + ) + ); + plumeProxy = pUSD(address(plumeProxyContract)); + + // Make Plume contracts and endpoints persistent on Plume fork + vm.makePersistent(address(plumeProxy)); + vm.makePersistent(address(plumeProxyContract)); + vm.makePersistent(PLUME_LZ_ENDPOINT); + + // Make contracts persistent on opposite forks + vm.selectFork(ethMainnetFork); + vm.makePersistent(address(plumeProxy)); + vm.makePersistent(PLUME_LZ_ENDPOINT); + + vm.selectFork(plumeMainnetFork); + vm.makePersistent(address(ethProxy)); + vm.makePersistent(ETH_LZ_ENDPOINT); + + // Cast proxies to both interfaces + ethVault = IBoringVaultAdapter(address(ethProxy)); + plumeVault = IBoringVaultAdapter(address(plumeProxy)); + ethOFT = OFTCore(address(ethProxy)); + plumeOFT = OFTCore(address(plumeProxy)); + + vm.selectFork(ethMainnetFork); + vm.startPrank(owner); + + // On ETH chain, set Plume as peer + bytes32 plumePeer = bytes32(uint256(uint160(address(plumeProxy)))); + ethOFT.setPeer(PLUME_EID, plumePeer); + + // Configure DVN for ETH -> Plume path + SetConfigParam[] memory params = new SetConfigParam[](1); + params[0] = SetConfigParam({ + lib: address(ethOFT), + config: abi.encode( + uint8(1), // number of confirmations required + address(0), // optional oracle address (0x0 for default) + uint256(0), // optional oracle fee (0 for default) + address(0), // optional relayer address (0x0 for default) + uint256(0) // optional relayer fee (0 for default) + ) + }); + ILayerZeroEndpointV2(ETH_LZ_ENDPOINT).setConfig(params); + vm.stopPrank(); + + // Set up peers and DVN configurations on Plume chain + vm.selectFork(plumeMainnetFork); + vm.startPrank(owner); + + // On Plume chain, set ETH as peer + bytes32 ethPeer = bytes32(uint256(uint160(address(ethProxy)))); + plumeOFT.setPeer(ETH_EID, ethPeer); + + // Configure DVN for Plume -> ETH path + params = new SetConfigParam[](1); + params[0] = SetConfigParam({ + lib: address(plumeOFT), + config: abi.encode( + uint8(1), // number of confirmations required + address(0), // optional oracle address (0x0 for default) + uint256(0), // optional oracle fee (0 for default) + address(0), // optional relayer address (0x0 for default) + uint256(0) // optional relayer fee (0 for default) + ) + }); + ILayerZeroEndpointV2(PLUME_LZ_ENDPOINT).setConfig(params); + vm.stopPrank(); +} - vm.selectFork(plumeMainnetFork); - uint256 chainId; - assembly { - chainId := chainid() - } - console.log("Plume mainnet forked, chainId:", chainId); - require(chainId == 98_865, "Not connected to Plume mainnet: chainId"); - - // Create users - owner = makeAddr("owner"); - user = makeAddr("user"); - - // Make contracts persistent - vm.makePersistent(ETH_pUSD); - vm.makePersistent(PLUME_pUSD); - vm.makePersistent(ETH_USDC); - vm.makePersistent(PLUME_USDC); - - // Use existing deployed contracts instead of deploying new ones - ethProxy = pUSD(ETH_pUSD); - plumeProxy = pUSD(PLUME_pUSD); - vault = IBoringVault(VAULT_TOKEN); - adapter = IBoringVaultAdapter(ETH_pUSD); // Need to add this constant - - // Verify contracts exist - require(address(ETH_pUSD).code.length > 0, "ETH pUSD not found"); - require(address(PLUME_pUSD).code.length > 0, "Plume pUSD not found"); + // Helper function to convert address to bytes32 + function addressToBytes32( + address _addr + ) internal pure returns (bytes32) { + return bytes32(uint256(uint160(_addr))); } function testInitialization() public { @@ -126,50 +237,63 @@ contract pUSD_LZTest is Test { function testCrossChainTransfer() public { // Start on Ethereum mainnet vm.selectFork(ethMainnetFork); - uint256 amount = 1e9; // 1000 pUSD + uint256 amount = 1e6; // 1 USDC (6 decimals) - // Give user some pUSD on Ethereum - deal(address(ethProxy), user, amount); + // Give user some USDC on Ethereum + deal(ETH_USDC, user, amount); vm.startPrank(user); - // Verify initial balance on Ethereum - assertEq(ethProxy.balanceOf(user), amount); + // Approve adapter to spend USDC + IERC20(ETH_USDC).approve(address(ethVault), amount); + + // Get expected shares using vault interface + uint256 minimumMint = ethVault.previewDeposit(amount); - // Perform cross-chain transfer to Plume - bytes memory options = ""; - ethProxy.sendFrom{ value: 1 ether }( - user, PLUME_EID, bytes32(uint256(uint160(user))), amount, payable(user), options + // First deposit USDC into adapter using vault interface + ethVault.deposit( + amount, + user, // receiver + user, // controller + minimumMint + ); + + // Then initiate cross-chain transfer using OFT interface + vm.deal(user, 1 ether); // For LZ fees + + // Create SendParam for OFT + SendParam memory params = SendParam({ + dstEid: PLUME_EID, + to: bytes32(uint256(uint160(user))), + amountLD: amount, + minAmountLD: amount, + extraOptions: bytes(""), + composeMsg: bytes(""), + oftCmd: bytes("") + }); + + // Get messaging fee + MessagingFee memory fee = ethOFT.quoteSend(params, false); + + // Send tokens cross-chain + ethOFT.send{ value: fee.nativeFee }( + params, + fee, + payable(user) // refundAddress ); vm.stopPrank(); - // Verify balance decreased on ETH mainnet - assertEq(ethProxy.balanceOf(user), 0); + // Verify USDC was taken from user on Ethereum + assertEq(IERC20(ETH_USDC).balanceOf(user), 0); // Switch to Plume mainnet to verify receipt vm.selectFork(plumeMainnetFork); - // Verify pUSD arrived on Plume - assertEq(plumeProxy.balanceOf(user), amount); - - // User needs to approve adapter to spend their pUSD - vm.startPrank(user); - plumeProxy.approve(address(adapter), amount); - - // Deposit into vault through adapter - uint256 minimumMint = adapter.previewDeposit(amount); // Get expected shares - uint256 shares = adapter.deposit( - amount, // assets - user, // receiver - user, // controller - minimumMint // minimumMint - ); - vm.stopPrank(); + // Verify user received shares in the Plume vault + assertEq(IBoringVault(VAULT_TOKEN).balanceOf(user), minimumMint); - // Verify vault shares - assertEq(vault.balanceOf(user), shares); // Verify adapter's record of user's assets - assertEq(adapter.assetsOf(user), amount); + assertEq(plumeVault.assetsOf(user), amount); } }