From 7cdbbab44a908ce7ea5c0d32123bb63b113add37 Mon Sep 17 00:00:00 2001 From: Csongor Kiss Date: Thu, 6 Jun 2024 20:04:35 +0100 Subject: [PATCH 01/10] evm: add support for per-chain enabled transceivers --- evm/src/NttManager/ManagerBase.sol | 39 ++++++++++++++++++++-- evm/src/NttManager/TransceiverRegistry.sol | 33 ++++++++++++++++++ evm/src/interfaces/IManagerBase.sol | 3 ++ 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/evm/src/NttManager/ManagerBase.sol b/evm/src/NttManager/ManagerBase.sol index 8a336fccc..46c1671f6 100644 --- a/evm/src/NttManager/ManagerBase.sol +++ b/evm/src/NttManager/ManagerBase.sol @@ -75,6 +75,19 @@ abstract contract ManagerBase is } } + function _getThresholdStoragePerChain() + private + pure + returns (mapping(uint16 => _Threshold) storage $) + { + // TODO: this is safe (reusing the storage slot, because the mapping + // doesn't write into the slot itself) buy maybe we shouldn't? + uint256 slot = uint256(THRESHOLD_SLOT); + assembly ("memory-safe") { + $.slot := slot + } + } + function _getMessageAttestationsStorage() internal pure @@ -122,6 +135,9 @@ abstract contract ManagerBase is uint256 totalPriceQuote = 0; for (uint256 i = 0; i < numEnabledTransceivers; i++) { address transceiverAddr = enabledTransceivers[i]; + if (!_isTransceiverEnabledForChain(transceiverAddr, recipientChain)) { + continue; + } uint8 registeredTransceiverIndex = transceiverInfos[transceiverAddr].index; uint256 transceiverPriceQuote = ITransceiver(transceiverAddr).quoteDeliveryPrice( recipientChain, transceiverInstructions[registeredTransceiverIndex] @@ -150,6 +166,7 @@ abstract contract ManagerBase is ) { revert TransceiverAlreadyAttestedToMessage(nttManagerMessageHash); } + _getMessageAttestationsStorage()[nttManagerMessageHash].sourceChainId = sourceChainId; _setTransceiverAttestedToMessage(nttManagerMessageHash, msg.sender); return nttManagerMessageHash; @@ -200,6 +217,9 @@ abstract contract ManagerBase is // call into transceiver contracts to send the message for (uint256 i = 0; i < numEnabledTransceivers; i++) { address transceiverAddr = enabledTransceivers[i]; + if (!_isTransceiverEnabledForChain(transceiverAddr, recipientChain)) { + continue; + } // send it to the recipient nttManager based on the chain ITransceiver(transceiverAddr).sendMessage{value: priceQuotes[i]}( @@ -284,11 +304,22 @@ abstract contract ManagerBase is return _getThresholdStorage().num; } + function getThreshold( + uint16 forChainId + ) public view returns (uint8) { + uint8 threshold = _getThresholdStoragePerChain()[forChainId].num; + if (threshold == 0) { + return _getThresholdStorage().num; + } + return threshold; + } + /// @inheritdoc IManagerBase function isMessageApproved( bytes32 digest ) public view returns (bool) { - uint8 threshold = getThreshold(); + uint16 sourceChainId = _getMessageAttestationsStorage()[digest].sourceChainId; + uint8 threshold = getThreshold(sourceChainId); return messageAttestations(digest) >= threshold && threshold > 0; } @@ -432,8 +463,10 @@ abstract contract ManagerBase is bytes32 digest ) internal view returns (uint64) { uint64 enabledTransceiverBitmap = _getEnabledTransceiversBitmap(); - return - _getMessageAttestationsStorage()[digest].attestedTransceivers & enabledTransceiverBitmap; + uint16 sourceChainId = _getMessageAttestationsStorage()[digest].sourceChainId; + uint64 enabledTransceiversForChain = _getEnabledTransceiversBitmapForChain(sourceChainId); + return _getMessageAttestationsStorage()[digest].attestedTransceivers + & enabledTransceiverBitmap & enabledTransceiversForChain; } function _getEnabledTransceiverAttestedToMessage( diff --git a/evm/src/NttManager/TransceiverRegistry.sol b/evm/src/NttManager/TransceiverRegistry.sol index d95f39b73..271b924db 100644 --- a/evm/src/NttManager/TransceiverRegistry.sol +++ b/evm/src/NttManager/TransceiverRegistry.sol @@ -122,6 +122,19 @@ abstract contract TransceiverRegistry { } } + function _getPerChainTransceiverBitmapStorage() + private + pure + returns (mapping(uint16 => _EnabledTransceiverBitmap) storage $) + { + // TODO: this is safe (reusing the storage slot, because the mapping + // doesn't write into the slot itself) buy maybe we shouldn't? + uint256 slot = uint256(TRANSCEIVER_BITMAP_SLOT); + assembly ("memory-safe") { + $.slot := slot + } + } + function _getRegisteredTransceiversStorage() internal pure returns (address[] storage $) { uint256 slot = uint256(REGISTERED_TRANSCEIVERS_SLOT); assembly ("memory-safe") { @@ -138,6 +151,15 @@ abstract contract TransceiverRegistry { // =============== Storage Getters/Setters ======================================== + function _isTransceiverEnabledForChain( + address transceiver, + uint16 chainId + ) internal view returns (bool) { + uint64 bitmap = _getEnabledTransceiversBitmapForChain(chainId); + uint8 index = _getTransceiverInfosStorage()[transceiver].index; + return (bitmap & uint64(1 << index)) != 0; + } + function _setTransceiver( address transceiver ) internal returns (uint8 index) { @@ -234,6 +256,17 @@ abstract contract TransceiverRegistry { return _getTransceiverBitmapStorage().bitmap; } + function _getEnabledTransceiversBitmapForChain( + uint16 forChainId + ) internal view virtual returns (uint64 bitmap) { + bitmap = _getPerChainTransceiverBitmapStorage()[forChainId].bitmap; + if (bitmap == 0) { + // NOTE: this makes it backwards compatible -- if the bitmap is not + // set, it's assumed the corridor uses all transceivers. + return type(uint64).max; + } + } + /// @notice Returns the Transceiver contracts that have been enabled via governance. function getTransceivers() external pure returns (address[] memory result) { result = _getEnabledTransceiversStorage(); diff --git a/evm/src/interfaces/IManagerBase.sol b/evm/src/interfaces/IManagerBase.sol index c4d1d565f..915720fe4 100644 --- a/evm/src/interfaces/IManagerBase.sol +++ b/evm/src/interfaces/IManagerBase.sol @@ -21,6 +21,9 @@ interface IManagerBase { struct AttestationInfo { bool executed; uint64 attestedTransceivers; + // TODO: think about potential migration issues here (this field was + // introduced later, so for older messages it will be 0). + uint16 sourceChainId; } struct _Sequence { From 7ba3b74c6e59cbddb43c0cc1417ae1fb6799aab3 Mon Sep 17 00:00:00 2001 From: Bruce Riley Date: Mon, 16 Sep 2024 12:44:18 -0500 Subject: [PATCH 02/10] EVM/Add setter and tests for per-chain thresholds --- evm/src/NttManager/ManagerBase.sol | 20 +- evm/src/interfaces/IManagerBase.sol | 18 + evm/test/IntegrationPerChainThresholds.t.sol | 582 +++++++++++++++++++ 3 files changed, 619 insertions(+), 1 deletion(-) create mode 100755 evm/test/IntegrationPerChainThresholds.t.sol diff --git a/evm/src/NttManager/ManagerBase.sol b/evm/src/NttManager/ManagerBase.sol index 46c1671f6..e4128f307 100644 --- a/evm/src/NttManager/ManagerBase.sol +++ b/evm/src/NttManager/ManagerBase.sol @@ -443,6 +443,21 @@ abstract contract ManagerBase is emit ThresholdChanged(oldThreshold, threshold); } + + /// @inheritdoc IManagerBase + function setThresholdPerChain(uint16 forChainId, uint8 threshold) external onlyOwner { + if (threshold == 0) { + revert ZeroThreshold(); + } + + mapping(uint16 => _Threshold) storage _threshold = _getThresholdStoragePerChain(); + uint8 oldThreshold = _threshold[forChainId].num; + + _threshold[forChainId].num = threshold; + _checkThresholdInvariants(_threshold[forChainId].num); + + emit PerChainThresholdChanged(forChainId, oldThreshold, threshold); + } // =============== Internal ============================================================== @@ -515,7 +530,10 @@ abstract contract ManagerBase is } function _checkThresholdInvariants() internal view { - uint8 threshold = _getThresholdStorage().num; + _checkThresholdInvariants(_getThresholdStorage().num); + } + + function _checkThresholdInvariants(uint8 threshold) internal pure { _NumTransceivers memory numTransceivers = _getNumTransceiversStorage(); // invariant: threshold <= enabledTransceivers.length diff --git a/evm/src/interfaces/IManagerBase.sol b/evm/src/interfaces/IManagerBase.sol index 915720fe4..5733b2031 100644 --- a/evm/src/interfaces/IManagerBase.sol +++ b/evm/src/interfaces/IManagerBase.sol @@ -48,6 +48,14 @@ interface IManagerBase { /// @param oldThreshold The old threshold. /// @param threshold The new threshold. event ThresholdChanged(uint8 oldThreshold, uint8 threshold); + + /// @notice Emmitted when the per-chain threshold required transceivers is changed. + /// @dev Topic0 + /// 0x2a855b929b9a53c6fb5b5ed248b27e502b709c088e036a5aa17620c8fc5085a9. + /// @param chainId The chain to which the threshold applies + /// @param oldThreshold The old threshold. + /// @param threshold The new threshold. + event PerChainThresholdChanged(uint16 chainId, uint8 oldThreshold, uint8 threshold); /// @notice Emitted when an transceiver is removed from the nttManager. /// @dev Topic0 @@ -128,6 +136,16 @@ interface IManagerBase { function setThreshold( uint8 threshold ) external; + + /// @notice Sets the per-chain threshold for the number of attestations required for a message + /// to be considered valid. Note that if a threshold is not specified for a chain, the default applies. + /// @param chainId The chain for which the threshold applies + /// @param threshold The new threshold. + /// @dev This method can only be executed by the `owner`. + function setThresholdPerChain( + uint16 chainId, + uint8 threshold + ) external; /// @notice Sets the transceiver for the given chain. /// @param transceiver The address of the transceiver. diff --git a/evm/test/IntegrationPerChainThresholds.t.sol b/evm/test/IntegrationPerChainThresholds.t.sol new file mode 100755 index 000000000..ab8ff8fe7 --- /dev/null +++ b/evm/test/IntegrationPerChainThresholds.t.sol @@ -0,0 +1,582 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity >=0.8.8 <0.9.0; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +import "../src/NttManager/NttManager.sol"; +import "../src/Transceiver/Transceiver.sol"; +import "../src/interfaces/INttManager.sol"; +import "../src/interfaces/IRateLimiter.sol"; +import "../src/interfaces/ITransceiver.sol"; +import "../src/interfaces/IManagerBase.sol"; +import "../src/interfaces/IRateLimiterEvents.sol"; +import {Utils} from "./libraries/Utils.sol"; +import {DummyToken, DummyTokenMintAndBurn} from "./NttManager.t.sol"; +import "../src/interfaces/IWormholeTransceiver.sol"; +import {WormholeTransceiver} from "../src/Transceiver/WormholeTransceiver/WormholeTransceiver.sol"; +import "../src/libraries/TransceiverStructs.sol"; +import "./mocks/MockNttManager.sol"; +import "./mocks/MockTransceivers.sol"; + +import "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; +import "openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import "wormhole-solidity-sdk/interfaces/IWormhole.sol"; +import "wormhole-solidity-sdk/testing/helpers/WormholeSimulator.sol"; +import "wormhole-solidity-sdk/Utils.sol"; +//import "wormhole-solidity-sdk/testing/WormholeRelayerTest.sol"; + +contract TestPerChainThresholds is Test, IRateLimiterEvents { + NttManager nttManagerChain1; + NttManager nttManagerChain2; + NttManager nttManagerChain3; + + using TrimmedAmountLib for uint256; + using TrimmedAmountLib for TrimmedAmount; + + uint16 constant chainId1 = 7; + uint16 constant chainId2 = 100; + uint16 constant chainId3 = 101; + uint8 constant FAST_CONSISTENCY_LEVEL = 200; + uint256 constant GAS_LIMIT = 500000; + + uint16 constant SENDING_CHAIN_ID = 1; + uint256 constant DEVNET_GUARDIAN_PK = + 0xcfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0; + WormholeSimulator guardian; + uint256 initialBlockTimestamp; + + WormholeTransceiver wormholeTransceiverChain1; + WormholeTransceiver secondWormholeTransceiverChain1; + WormholeTransceiver wormholeTransceiverChain2; + WormholeTransceiver secondWormholeTransceiverChain2; + WormholeTransceiver wormholeTransceiverChain3; + WormholeTransceiver secondWormholeTransceiverChain3; + address userA = address(0x123); + address userB = address(0x456); + address userC = address(0x789); + address userD = address(0xABC); + + address relayer = address(0x28D8F1Be96f97C1387e94A53e00eCcFb4E75175a); + IWormhole wormhole = IWormhole(0x4a8bc80Ed5a4067f1CCf107057b8270E0cC11A78); + + // This function sets up the following config: + // - A manager on each of three chains. + // - Two transceivers on each chain, all interconnected as peers. + // - On chain one, it sets a default threshold of one and a per-chain threshold of two for chain three. + // - On chain three, it sets a default threshold of one and a per-chain threshold of two for chain one. + + function setUp() public { + string memory url = "https://ethereum-sepolia-rpc.publicnode.com"; + vm.createSelectFork(url); + initialBlockTimestamp = vm.getBlockTimestamp(); + + guardian = new WormholeSimulator(address(wormhole), DEVNET_GUARDIAN_PK); + + vm.chainId(chainId1); + DummyToken t1 = new DummyToken(); + NttManager implementation = new MockNttManagerContract( + address(t1), IManagerBase.Mode.LOCKING, chainId1, 1 days, false + ); + + nttManagerChain1 = + MockNttManagerContract(address(new ERC1967Proxy(address(implementation), ""))); + nttManagerChain1.initialize(); + + // Create the first transceiver, from chain 1 to chain 2. + WormholeTransceiver wormholeTransceiverChain1Implementation = new MockWormholeTransceiverContract( + address(nttManagerChain1), + address(wormhole), + address(relayer), + address(0x0), + FAST_CONSISTENCY_LEVEL, + GAS_LIMIT + ); + wormholeTransceiverChain1 = MockWormholeTransceiverContract( + address(new ERC1967Proxy(address(wormholeTransceiverChain1Implementation), "")) + ); + + // Only the deployer should be able to initialize + vm.prank(userA); + vm.expectRevert( + abi.encodeWithSelector(ITransceiver.UnexpectedDeployer.selector, address(this), userA) + ); + wormholeTransceiverChain1.initialize(); + + // Actually initialize properly now + wormholeTransceiverChain1.initialize(); + + nttManagerChain1.setTransceiver(address(wormholeTransceiverChain1)); + nttManagerChain1.setOutboundLimit(type(uint64).max); + nttManagerChain1.setInboundLimit(type(uint64).max, chainId2); + nttManagerChain1.setInboundLimit(type(uint64).max, chainId3); + + // Create the second transceiver for chain 1. + WormholeTransceiver secondWormholeTransceiverChain1Implementation = new MockWormholeTransceiverContract( + address(nttManagerChain1), + address(wormhole), + address(relayer), + address(0x0), + FAST_CONSISTENCY_LEVEL, + GAS_LIMIT + ); + secondWormholeTransceiverChain1 = MockWormholeTransceiverContract( + address(new ERC1967Proxy(address(secondWormholeTransceiverChain1Implementation), "")) + ); + + secondWormholeTransceiverChain1.initialize(); + nttManagerChain1.setTransceiver(address(secondWormholeTransceiverChain1)); + + // Chain 2 setup + vm.chainId(chainId2); + DummyToken t2 = new DummyTokenMintAndBurn(); + NttManager implementationChain2 = new MockNttManagerContract( + address(t2), IManagerBase.Mode.BURNING, chainId2, 1 days, false + ); + + nttManagerChain2 = + MockNttManagerContract(address(new ERC1967Proxy(address(implementationChain2), ""))); + nttManagerChain2.initialize(); + + WormholeTransceiver wormholeTransceiverChain2Implementation = new MockWormholeTransceiverContract( + address(nttManagerChain2), + address(wormhole), + address(relayer), + address(0x0), + FAST_CONSISTENCY_LEVEL, + GAS_LIMIT + ); + wormholeTransceiverChain2 = MockWormholeTransceiverContract( + address(new ERC1967Proxy(address(wormholeTransceiverChain2Implementation), "")) + ); + wormholeTransceiverChain2.initialize(); + + nttManagerChain2.setTransceiver(address(wormholeTransceiverChain2)); + nttManagerChain2.setOutboundLimit(type(uint64).max); + nttManagerChain2.setInboundLimit(type(uint64).max, chainId1); + + // Register peer contracts for the nttManager and transceiver. Transceivers and nttManager each have the concept of peers here. + nttManagerChain1.setPeer( + chainId2, bytes32(uint256(uint160(address(nttManagerChain2)))), 9, type(uint64).max + ); + nttManagerChain2.setPeer( + chainId1, bytes32(uint256(uint160(address(nttManagerChain1)))), 7, type(uint64).max + ); + + // Create the second transceiver for chain 2. + WormholeTransceiver secondWormholeTransceiverChain2Implementation = new MockWormholeTransceiverContract( + address(nttManagerChain2), + address(wormhole), + address(relayer), + address(0x0), + FAST_CONSISTENCY_LEVEL, + GAS_LIMIT + ); + secondWormholeTransceiverChain2 = MockWormholeTransceiverContract( + address(new ERC1967Proxy(address(secondWormholeTransceiverChain2Implementation), "")) + ); + + secondWormholeTransceiverChain2.initialize(); + nttManagerChain2.setTransceiver(address(secondWormholeTransceiverChain2)); + + // Set peers for the transceivers + wormholeTransceiverChain1.setWormholePeer( + chainId2, bytes32(uint256(uint160(address(wormholeTransceiverChain2)))) + ); + wormholeTransceiverChain2.setWormholePeer( + chainId1, bytes32(uint256(uint160(address(wormholeTransceiverChain1)))) + ); + secondWormholeTransceiverChain1.setWormholePeer( + chainId2, bytes32(uint256(uint160(address(secondWormholeTransceiverChain2)))) + ); + secondWormholeTransceiverChain2.setWormholePeer( + chainId1, bytes32(uint256(uint160(address(secondWormholeTransceiverChain1)))) + ); + + // Chain 3 setup + vm.chainId(chainId3); + DummyToken t3 = new DummyTokenMintAndBurn(); + NttManager implementationChain3 = new MockNttManagerContract( + address(t3), IManagerBase.Mode.BURNING, chainId3, 1 days, false + ); + + nttManagerChain3 = + MockNttManagerContract(address(new ERC1967Proxy(address(implementationChain3), ""))); + nttManagerChain3.initialize(); + + WormholeTransceiver wormholeTransceiverChain3Implementation = new MockWormholeTransceiverContract( + address(nttManagerChain3), + address(wormhole), + address(relayer), + address(0x0), + FAST_CONSISTENCY_LEVEL, + GAS_LIMIT + ); + wormholeTransceiverChain3 = MockWormholeTransceiverContract( + address(new ERC1967Proxy(address(wormholeTransceiverChain3Implementation), "")) + ); + wormholeTransceiverChain3.initialize(); + + nttManagerChain3.setTransceiver(address(wormholeTransceiverChain3)); + nttManagerChain3.setOutboundLimit(type(uint64).max); + nttManagerChain3.setInboundLimit(type(uint64).max, chainId1); + + // Register peer contracts for the nttManager and transceiver. Transceivers and nttManager each have the concept of peers here. + nttManagerChain1.setPeer( + chainId3, bytes32(uint256(uint160(address(nttManagerChain3)))), 9, type(uint64).max + ); + nttManagerChain3.setPeer( + chainId1, bytes32(uint256(uint160(address(nttManagerChain1)))), 7, type(uint64).max + ); + + // Create the second transceiver, from chain 3 to chain 1. + WormholeTransceiver secondWormholeTransceiverChain3Implementation = new MockWormholeTransceiverContract( + address(nttManagerChain3), + address(wormhole), + address(relayer), + address(0x0), + FAST_CONSISTENCY_LEVEL, + GAS_LIMIT + ); + secondWormholeTransceiverChain3 = MockWormholeTransceiverContract( + address(new ERC1967Proxy(address(secondWormholeTransceiverChain3Implementation), "")) + ); + + // Actually initialize properly now + secondWormholeTransceiverChain3.initialize(); + + nttManagerChain3.setTransceiver(address(secondWormholeTransceiverChain3)); + nttManagerChain1.setOutboundLimit(type(uint64).max); + nttManagerChain1.setInboundLimit(type(uint64).max, chainId3); + + // Set peers for the transceivers + wormholeTransceiverChain1.setWormholePeer( + chainId3, bytes32(uint256(uint160(address(wormholeTransceiverChain3)))) + ); + wormholeTransceiverChain3.setWormholePeer( + chainId1, bytes32(uint256(uint160(address(wormholeTransceiverChain1)))) + ); + wormholeTransceiverChain3.setWormholePeer( + chainId2, bytes32(uint256(uint160(address(wormholeTransceiverChain2)))) + ); + wormholeTransceiverChain2.setWormholePeer( + chainId3, bytes32(uint256(uint160(address(wormholeTransceiverChain3)))) + ); + secondWormholeTransceiverChain1.setWormholePeer( + chainId3, bytes32(uint256(uint160(address(secondWormholeTransceiverChain3)))) + ); + secondWormholeTransceiverChain3.setWormholePeer( + chainId1, bytes32(uint256(uint160(address(secondWormholeTransceiverChain1)))) + ); + secondWormholeTransceiverChain2.setWormholePeer( + chainId3, bytes32(uint256(uint160(address(secondWormholeTransceiverChain3)))) + ); + secondWormholeTransceiverChain3.setWormholePeer( + chainId2, bytes32(uint256(uint160(address(secondWormholeTransceiverChain2)))) + ); + + // Set the thresholds. + require(nttManagerChain1.getThreshold() != 0, "Threshold is zero with active transceivers"); + + // Actually set it + nttManagerChain1.setThreshold(1); + nttManagerChain2.setThreshold(1); + nttManagerChain3.setThreshold(1); + + // On chain 1, set the threshold to chain 3 to be different. + nttManagerChain1.setThresholdPerChain(chainId3, 2); + nttManagerChain3.setThresholdPerChain(chainId1, 2); + + require(nttManagerChain1.getThreshold() == 1, "Default threshold is wrong"); + require(nttManagerChain1.getThreshold(chainId3) == 2, "Threshold for chain 3 is wrong"); + } + + // This test does a transfer between chain one and chain two. + // Since chain two uses the default threshold, posting a VAA from only one transceiver completes the transfer. + function test_defaultThreshold() public { + vm.chainId(chainId1); + + // Setting up the transfer + DummyToken token1 = DummyToken(nttManagerChain1.token()); + DummyToken token2 = DummyTokenMintAndBurn(nttManagerChain2.token()); + + uint8 decimals = token1.decimals(); + uint256 sendingAmount = 5 * 10 ** decimals; + token1.mintDummy(address(userA), 5 * 10 ** decimals); + + // Transfer tokens from chain one to chain two through standard means (not relayer) + vm.startPrank(userA); + token1.approve(address(nttManagerChain1), sendingAmount); + vm.recordLogs(); + { + uint256 nttManagerBalanceBefore = token1.balanceOf(address(nttManagerChain1)); + uint256 userBalanceBefore = token1.balanceOf(address(userA)); + nttManagerChain1.transfer(sendingAmount, chainId2, bytes32(uint256(uint160(userB)))); + + // Balance check on funds going in and out working as expected + uint256 nttManagerBalanceAfter = token1.balanceOf(address(nttManagerChain1)); + uint256 userBalanceAfter = token1.balanceOf(address(userB)); + require( + nttManagerBalanceBefore + sendingAmount == nttManagerBalanceAfter, + "Should be locking the tokens" + ); + require( + userBalanceBefore - sendingAmount == userBalanceAfter, + "User should have sent tokens" + ); + } + + vm.stopPrank(); + + // Get and sign the log to go down the other pipes. There should be two messages since we have two transceivers. + Vm.Log[] memory entries = guardian.fetchWormholeMessageFromLog(vm.getRecordedLogs()); + require(2 == entries.length, "Unexpected number of log entries 1"); + bytes[] memory encodedVMs = new bytes[](entries.length); + for (uint256 i = 0; i < encodedVMs.length; i++) { + encodedVMs[i] = guardian.fetchSignedMessageFromLogs(entries[i], chainId1); + } + + // Chain2 verification and checks + vm.chainId(chainId2); + + { + uint256 supplyBefore = token2.totalSupply(); + wormholeTransceiverChain2.receiveMessage(encodedVMs[0]); + uint256 supplyAfter = token2.totalSupply(); + + require(sendingAmount + supplyBefore == supplyAfter, "Supplies dont match"); + require(token2.balanceOf(userB) == sendingAmount, "User didn't receive tokens"); + require( + token2.balanceOf(address(nttManagerChain2)) == 0, "NttManager has unintended funds" + ); + } + + // Go back the other way from a THIRD user + vm.prank(userB); + token2.transfer(userC, sendingAmount); + + vm.startPrank(userC); + token2.approve(address(nttManagerChain2), sendingAmount); + vm.recordLogs(); + + // Supply checks on the transfer + { + uint256 supplyBefore = token2.totalSupply(); + nttManagerChain2.transfer( + sendingAmount, + chainId1, + toWormholeFormat(userD), + toWormholeFormat(userC), + false, + encodeTransceiverInstruction(true) + ); + + uint256 supplyAfter = token2.totalSupply(); + + require(sendingAmount - supplyBefore == supplyAfter, "Supplies don't match"); + require(token2.balanceOf(userB) == 0, "OG user receive tokens"); + require(token2.balanceOf(userC) == 0, "Sending user didn't receive tokens"); + require( + token2.balanceOf(address(nttManagerChain2)) == 0, + "NttManager didn't receive unintended funds" + ); + } + + // Get and sign the log to go down the other pipe. Thank you to whoever wrote this code in the past! + entries = guardian.fetchWormholeMessageFromLog(vm.getRecordedLogs()); + require(2 == entries.length, "Unexpected number of log entries 2"); + encodedVMs = new bytes[](entries.length); + for (uint256 i = 0; i < encodedVMs.length; i++) { + encodedVMs[i] = guardian.fetchSignedMessageFromLogs(entries[i], chainId2); + } + + vm.chainId(chainId1); + + // Receive the first message. Since the threshold is set to one, the transfer should complete. + { + uint256 supplyBefore = token1.totalSupply(); + wormholeTransceiverChain1.receiveMessage(encodedVMs[0]); + uint256 supplyAfter = token1.totalSupply(); + + require(supplyBefore == supplyAfter, "Supplies don't match between operations"); + require(token1.balanceOf(userB) == 0, "OG user receive tokens"); + require(token1.balanceOf(userC) == 0, "Sending user didn't receive tokens"); + require(token1.balanceOf(userD) == sendingAmount, "User received funds"); + } + + // Receive the second message. That should do nothing since we've already processed the transfer. + { + uint256 supplyBefore = token1.totalSupply(); + secondWormholeTransceiverChain1.receiveMessage(encodedVMs[1]); + uint256 supplyAfter = token1.totalSupply(); + + require(supplyBefore == supplyAfter, "Supplies don't match between operations"); + require(token1.balanceOf(userB) == 0, "OG user receive tokens"); + require(token1.balanceOf(userC) == 0, "Sending user didn't receive tokens"); + require(token1.balanceOf(userD) == sendingAmount, "User received funds"); + } + } + + // This test does a transfer between chain one and chain three. + // Since the threshold for these two chains is two, the transfer is not completed until both VAAs are posted. + function test_perChainThreshold() public { + vm.chainId(chainId1); + + // Setting up the transfer + DummyToken token1 = DummyToken(nttManagerChain1.token()); + DummyToken token3 = DummyTokenMintAndBurn(nttManagerChain3.token()); + + uint8 decimals = token1.decimals(); + uint256 sendingAmount = 5 * 10 ** decimals; + token1.mintDummy(address(userA), 5 * 10 ** decimals); + vm.startPrank(userA); + token1.approve(address(nttManagerChain1), sendingAmount); + + vm.recordLogs(); + + // Send token from chain 1 to chain 3, userB. + { + uint256 nttManagerBalanceBefore = token1.balanceOf(address(nttManagerChain1)); + uint256 userBalanceBefore = token1.balanceOf(address(userA)); + nttManagerChain1.transfer(sendingAmount, chainId3, bytes32(uint256(uint160(userB)))); + + // Balance check on funds going in and out working as expected + uint256 nttManagerBalanceAfter = token1.balanceOf(address(nttManagerChain1)); + uint256 userBalanceAfter = token1.balanceOf(address(userB)); + require( + nttManagerBalanceBefore + sendingAmount == nttManagerBalanceAfter, + "Should be locking the tokens" + ); + require( + userBalanceBefore - sendingAmount == userBalanceAfter, + "User should have sent tokens" + ); + } + + vm.stopPrank(); + + // Get and sign the log to go down the other pipes. There should be two messages since we have two transceivers. + Vm.Log[] memory entries = guardian.fetchWormholeMessageFromLog(vm.getRecordedLogs()); + require(2 == entries.length, "Unexpected number of log entries 3"); + bytes[] memory encodedVMs = new bytes[](entries.length); + for (uint256 i = 0; i < encodedVMs.length; i++) { + encodedVMs[i] = guardian.fetchSignedMessageFromLogs(entries[i], chainId1); + } + + // Chain3 verification and checks + vm.chainId(chainId3); + + uint256 supplyBefore = token3.totalSupply(); + + // Submit the first message on chain 3. The numbers shouldn't change yet since the threshold is two. + wormholeTransceiverChain3.receiveMessage(encodedVMs[0]); + uint256 supplyAfter = token3.totalSupply(); + + require(supplyBefore == supplyAfter, "Supplies changed early"); + require(token3.balanceOf(userB) == 0, "User receive tokens early"); + require(token3.balanceOf(address(nttManagerChain3)) == 0, "NttManager has unintended funds"); + + // Submit the second message and the transfer should complete. + secondWormholeTransceiverChain3.receiveMessage(encodedVMs[1]); + supplyAfter = token3.totalSupply(); + + require(sendingAmount + supplyBefore == supplyAfter, "Supplies dont match"); + require(token3.balanceOf(userB) == sendingAmount, "User didn't receive tokens"); + require(token3.balanceOf(address(nttManagerChain3)) == 0, "NttManager has unintended funds"); + + // Go back the other way from a THIRD user + vm.prank(userB); + token3.transfer(userC, sendingAmount); + + vm.startPrank(userC); + token3.approve(address(nttManagerChain3), sendingAmount); + vm.recordLogs(); + + // Supply checks on the transfer + supplyBefore = token3.totalSupply(); + nttManagerChain3.transfer( + sendingAmount, + chainId1, + toWormholeFormat(userD), + toWormholeFormat(userC), + false, + encodeTransceiverInstruction(true) + ); + + supplyAfter = token3.totalSupply(); + + require(sendingAmount - supplyBefore == supplyAfter, "Supplies don't match"); + require(token3.balanceOf(userB) == 0, "OG user receive tokens"); + require(token3.balanceOf(userC) == 0, "Sending user didn't receive tokens"); + require( + token3.balanceOf(address(nttManagerChain3)) == 0, + "NttManager didn't receive unintended funds" + ); + + // Get and sign the log to go down the other pipe. Thank you to whoever wrote this code in the past! + entries = guardian.fetchWormholeMessageFromLog(vm.getRecordedLogs()); + require(2 == entries.length, "Unexpected number of log entries for response"); + encodedVMs = new bytes[](entries.length); + for (uint256 i = 0; i < encodedVMs.length; i++) { + encodedVMs[i] = guardian.fetchSignedMessageFromLogs(entries[i], chainId3); + } + + // Chain1 verification and checks with the receiving of the message + vm.chainId(chainId1); + + // Submit the first message back on chain one. Nothing should happen because our threshold is two. + supplyBefore = token1.totalSupply(); + wormholeTransceiverChain1.receiveMessage(encodedVMs[0]); + supplyAfter = token1.totalSupply(); + + require(supplyBefore == supplyAfter, "Supplies don't match between operations"); + require(token1.balanceOf(userB) == 0, "OG user receive tokens"); + require(token1.balanceOf(userC) == 0, "Sending user didn't receive tokens"); + require(token1.balanceOf(userD) == 0, "User received funds before they should"); + + // Submit the second message back on chain one. This should update the balance. + supplyBefore = token1.totalSupply(); + secondWormholeTransceiverChain1.receiveMessage(encodedVMs[1]); + supplyAfter = token1.totalSupply(); + + require(supplyBefore == supplyAfter, "Supplies don't match between operations"); /////////////////// Is this right?? + require(token1.balanceOf(userB) == 0, "OG user receive tokens"); + require(token1.balanceOf(userC) == 0, "Sending user didn't receive tokens"); + require(token1.balanceOf(userD) == sendingAmount, "User received funds"); + } + + function encodeTransceiverInstruction(bool relayer_off) public view returns (bytes memory) { + WormholeTransceiver.WormholeTransceiverInstruction memory instruction = + IWormholeTransceiver.WormholeTransceiverInstruction(relayer_off); + bytes memory encodedInstructionWormhole = + wormholeTransceiverChain1.encodeWormholeTransceiverInstruction(instruction); + TransceiverStructs.TransceiverInstruction memory TransceiverInstruction = TransceiverStructs + .TransceiverInstruction({index: 0, payload: encodedInstructionWormhole}); + TransceiverStructs.TransceiverInstruction[] memory TransceiverInstructions = + new TransceiverStructs.TransceiverInstruction[](1); + TransceiverInstructions[0] = TransceiverInstruction; + return TransceiverStructs.encodeTransceiverInstructions(TransceiverInstructions); + } + + // Encode an instruction for each of the relayers + function encodeTransceiverInstructions(bool relayer_off) public view returns (bytes memory) { + WormholeTransceiver.WormholeTransceiverInstruction memory instruction = + IWormholeTransceiver.WormholeTransceiverInstruction(relayer_off); + + bytes memory encodedInstructionWormhole = + wormholeTransceiverChain1.encodeWormholeTransceiverInstruction(instruction); + + TransceiverStructs.TransceiverInstruction memory TransceiverInstruction1 = + TransceiverStructs.TransceiverInstruction({index: 0, payload: encodedInstructionWormhole}); + TransceiverStructs.TransceiverInstruction memory TransceiverInstruction2 = + TransceiverStructs.TransceiverInstruction({index: 1, payload: encodedInstructionWormhole}); + + TransceiverStructs.TransceiverInstruction[] memory TransceiverInstructions = + new TransceiverStructs.TransceiverInstruction[](2); + + TransceiverInstructions[0] = TransceiverInstruction1; + TransceiverInstructions[1] = TransceiverInstruction2; + + return TransceiverStructs.encodeTransceiverInstructions(TransceiverInstructions); + } +} From c3283998091e0cd1cc1d6691a193353406610038 Mon Sep 17 00:00:00 2001 From: Bruce Riley Date: Tue, 17 Sep 2024 15:52:29 -0500 Subject: [PATCH 03/10] EVM/ Add support for per-chain transceivers --- evm/src/NttManager/ManagerBase.sol | 9 + evm/src/NttManager/TransceiverRegistry.sol | 18 + evm/src/interfaces/IManagerBase.sol | 20 +- evm/test/IntegrationPerChainThresholds.t.sol | 212 ++++++- .../IntegrationPerChainTransceivers.t.sol | 582 ++++++++++++++++++ 5 files changed, 810 insertions(+), 31 deletions(-) mode change 100755 => 100644 evm/test/IntegrationPerChainThresholds.t.sol create mode 100755 evm/test/IntegrationPerChainTransceivers.t.sol diff --git a/evm/src/NttManager/ManagerBase.sol b/evm/src/NttManager/ManagerBase.sol index e4128f307..39522fe70 100644 --- a/evm/src/NttManager/ManagerBase.sol +++ b/evm/src/NttManager/ManagerBase.sol @@ -379,6 +379,15 @@ abstract contract ManagerBase is ITransceiver(_registeredTransceivers[i]).transferTransceiverOwnership(newOwner); } } + + /// @inheritdoc IManagerBase + function enableTransceiverForChain( + address transceiver, + uint16 forChainId + ) external onlyOwner { + _enableTransceiverForChain(transceiver, forChainId); + emit TransceiverEnabledForChain(transceiver, forChainId); + } /// @inheritdoc IManagerBase function setTransceiver( diff --git a/evm/src/NttManager/TransceiverRegistry.sol b/evm/src/NttManager/TransceiverRegistry.sol index 271b924db..7aa02f13e 100644 --- a/evm/src/NttManager/TransceiverRegistry.sol +++ b/evm/src/NttManager/TransceiverRegistry.sol @@ -159,6 +159,24 @@ abstract contract TransceiverRegistry { uint8 index = _getTransceiverInfosStorage()[transceiver].index; return (bitmap & uint64(1 << index)) != 0; } + + function _enableTransceiverForChain( + address transceiver, + uint16 chainId + ) internal { + if (transceiver == address(0)) { + revert InvalidTransceiverZeroAddress(); + } + + mapping(address => TransceiverInfo) storage transceiverInfos = _getTransceiverInfosStorage(); + if (!transceiverInfos[transceiver].registered) { + revert NonRegisteredTransceiver(transceiver); + } + + uint8 index = _getTransceiverInfosStorage()[transceiver].index; + mapping(uint16 => _EnabledTransceiverBitmap)storage _bitmaps = _getPerChainTransceiverBitmapStorage(); + _bitmaps[chainId].bitmap |= uint64(1 << index); + } function _setTransceiver( address transceiver diff --git a/evm/src/interfaces/IManagerBase.sol b/evm/src/interfaces/IManagerBase.sol index 5733b2031..8b255e77e 100644 --- a/evm/src/interfaces/IManagerBase.sol +++ b/evm/src/interfaces/IManagerBase.sol @@ -52,7 +52,7 @@ interface IManagerBase { /// @notice Emmitted when the per-chain threshold required transceivers is changed. /// @dev Topic0 /// 0x2a855b929b9a53c6fb5b5ed248b27e502b709c088e036a5aa17620c8fc5085a9. - /// @param chainId The chain to which the threshold applies + /// @param chainId The chain to which the threshold applies. /// @param oldThreshold The old threshold. /// @param threshold The new threshold. event PerChainThresholdChanged(uint16 chainId, uint8 oldThreshold, uint8 threshold); @@ -64,6 +64,13 @@ interface IManagerBase { /// @param transceiversNum The current number of transceivers. /// @param threshold The current threshold of transceivers. event TransceiverAdded(address transceiver, uint256 transceiversNum, uint8 threshold); + + /// @notice Emitted when a transceiver is enabled for a chain. + /// @dev Topic0 + /// 0xf05962b5774c658e85ed80c91a75af9d66d2af2253dda480f90bce78aff5eda5. + /// @param transceiver The address of the transceiver. + /// @param chainId The chain to which the threshold applies. + event TransceiverEnabledForChain(address transceiver, uint16 chainId); /// @notice Emitted when an transceiver is removed from the nttManager. /// @dev Topic0 @@ -139,7 +146,7 @@ interface IManagerBase { /// @notice Sets the per-chain threshold for the number of attestations required for a message /// to be considered valid. Note that if a threshold is not specified for a chain, the default applies. - /// @param chainId The chain for which the threshold applies + /// @param chainId The chain for which the threshold applies. /// @param threshold The new threshold. /// @dev This method can only be executed by the `owner`. function setThresholdPerChain( @@ -160,6 +167,15 @@ interface IManagerBase { function removeTransceiver( address transceiver ) external; + + /// @notice Enables the transceiver for the given chain. + /// @param transceiver The address of the transceiver. + /// @param chainId The chain for which the threshold applies. + /// @dev This method can only be executed by the `owner`. + function enableTransceiverForChain( + address transceiver, + uint16 chainId + ) external; /// @notice Checks if a message has been approved. The message should have at least /// the minimum threshold of attestations from distinct endpoints. diff --git a/evm/test/IntegrationPerChainThresholds.t.sol b/evm/test/IntegrationPerChainThresholds.t.sol old mode 100755 new mode 100644 index ab8ff8fe7..8f1ce529e --- a/evm/test/IntegrationPerChainThresholds.t.sol +++ b/evm/test/IntegrationPerChainThresholds.t.sol @@ -26,7 +26,7 @@ import "wormhole-solidity-sdk/testing/helpers/WormholeSimulator.sol"; import "wormhole-solidity-sdk/Utils.sol"; //import "wormhole-solidity-sdk/testing/WormholeRelayerTest.sol"; -contract TestPerChainThresholds is Test, IRateLimiterEvents { +contract TestPerChainTransceivers is Test, IRateLimiterEvents { NttManager nttManagerChain1; NttManager nttManagerChain2; NttManager nttManagerChain3; @@ -106,11 +106,6 @@ contract TestPerChainThresholds is Test, IRateLimiterEvents { // Actually initialize properly now wormholeTransceiverChain1.initialize(); - nttManagerChain1.setTransceiver(address(wormholeTransceiverChain1)); - nttManagerChain1.setOutboundLimit(type(uint64).max); - nttManagerChain1.setInboundLimit(type(uint64).max, chainId2); - nttManagerChain1.setInboundLimit(type(uint64).max, chainId3); - // Create the second transceiver for chain 1. WormholeTransceiver secondWormholeTransceiverChain1Implementation = new MockWormholeTransceiverContract( address(nttManagerChain1), @@ -125,7 +120,6 @@ contract TestPerChainThresholds is Test, IRateLimiterEvents { ); secondWormholeTransceiverChain1.initialize(); - nttManagerChain1.setTransceiver(address(secondWormholeTransceiverChain1)); // Chain 2 setup vm.chainId(chainId2); @@ -151,10 +145,6 @@ contract TestPerChainThresholds is Test, IRateLimiterEvents { ); wormholeTransceiverChain2.initialize(); - nttManagerChain2.setTransceiver(address(wormholeTransceiverChain2)); - nttManagerChain2.setOutboundLimit(type(uint64).max); - nttManagerChain2.setInboundLimit(type(uint64).max, chainId1); - // Register peer contracts for the nttManager and transceiver. Transceivers and nttManager each have the concept of peers here. nttManagerChain1.setPeer( chainId2, bytes32(uint256(uint160(address(nttManagerChain2)))), 9, type(uint64).max @@ -177,7 +167,6 @@ contract TestPerChainThresholds is Test, IRateLimiterEvents { ); secondWormholeTransceiverChain2.initialize(); - nttManagerChain2.setTransceiver(address(secondWormholeTransceiverChain2)); // Set peers for the transceivers wormholeTransceiverChain1.setWormholePeer( @@ -217,10 +206,6 @@ contract TestPerChainThresholds is Test, IRateLimiterEvents { ); wormholeTransceiverChain3.initialize(); - nttManagerChain3.setTransceiver(address(wormholeTransceiverChain3)); - nttManagerChain3.setOutboundLimit(type(uint64).max); - nttManagerChain3.setInboundLimit(type(uint64).max, chainId1); - // Register peer contracts for the nttManager and transceiver. Transceivers and nttManager each have the concept of peers here. nttManagerChain1.setPeer( chainId3, bytes32(uint256(uint160(address(nttManagerChain3)))), 9, type(uint64).max @@ -245,10 +230,6 @@ contract TestPerChainThresholds is Test, IRateLimiterEvents { // Actually initialize properly now secondWormholeTransceiverChain3.initialize(); - nttManagerChain3.setTransceiver(address(secondWormholeTransceiverChain3)); - nttManagerChain1.setOutboundLimit(type(uint64).max); - nttManagerChain1.setInboundLimit(type(uint64).max, chainId3); - // Set peers for the transceivers wormholeTransceiverChain1.setWormholePeer( chainId3, bytes32(uint256(uint160(address(wormholeTransceiverChain3)))) @@ -274,11 +255,18 @@ contract TestPerChainThresholds is Test, IRateLimiterEvents { secondWormholeTransceiverChain3.setWormholePeer( chainId2, bytes32(uint256(uint160(address(secondWormholeTransceiverChain2)))) ); + } - // Set the thresholds. - require(nttManagerChain1.getThreshold() != 0, "Threshold is zero with active transceivers"); + // This test does a transfer between chain one and chain two where there are two transceivers. + // Since chain two uses the default threshold of one, posting a VAA from only one transceiver completes the transfer. + function test_defaultThresholds() public { + nttManagerChain1.setTransceiver(address(wormholeTransceiverChain1)); + nttManagerChain1.setTransceiver(address(secondWormholeTransceiverChain1)); + nttManagerChain2.setTransceiver(address(wormholeTransceiverChain2)); + nttManagerChain2.setTransceiver(address(secondWormholeTransceiverChain2)); + nttManagerChain3.setTransceiver(address(wormholeTransceiverChain3)); + nttManagerChain3.setTransceiver(address(secondWormholeTransceiverChain3)); - // Actually set it nttManagerChain1.setThreshold(1); nttManagerChain2.setThreshold(1); nttManagerChain3.setThreshold(1); @@ -289,11 +277,7 @@ contract TestPerChainThresholds is Test, IRateLimiterEvents { require(nttManagerChain1.getThreshold() == 1, "Default threshold is wrong"); require(nttManagerChain1.getThreshold(chainId3) == 2, "Threshold for chain 3 is wrong"); - } - // This test does a transfer between chain one and chain two. - // Since chain two uses the default threshold, posting a VAA from only one transceiver completes the transfer. - function test_defaultThreshold() public { vm.chainId(chainId1); // Setting up the transfer @@ -417,9 +401,27 @@ contract TestPerChainThresholds is Test, IRateLimiterEvents { } } - // This test does a transfer between chain one and chain three. - // Since the threshold for these two chains is two, the transfer is not completed until both VAAs are posted. + // This test does a transfer between chain one and chain three where there are two transceivers and the per-chain threshold is two. + // The transfer is not completed until both VAAs are posted. function test_perChainThreshold() public { + nttManagerChain1.setTransceiver(address(wormholeTransceiverChain1)); + nttManagerChain1.setTransceiver(address(secondWormholeTransceiverChain1)); + nttManagerChain2.setTransceiver(address(wormholeTransceiverChain2)); + nttManagerChain2.setTransceiver(address(secondWormholeTransceiverChain2)); + nttManagerChain3.setTransceiver(address(wormholeTransceiverChain3)); + nttManagerChain3.setTransceiver(address(secondWormholeTransceiverChain3)); + + nttManagerChain1.setThreshold(1); + nttManagerChain2.setThreshold(1); + nttManagerChain3.setThreshold(1); + + // On chain 1, set the threshold to chain 3 to be different. + nttManagerChain1.setThresholdPerChain(chainId3, 2); + nttManagerChain3.setThresholdPerChain(chainId1, 2); + + require(nttManagerChain1.getThreshold() == 1, "Default threshold is wrong"); + require(nttManagerChain1.getThreshold(chainId3) == 2, "Threshold for chain 3 is wrong"); + vm.chainId(chainId1); // Setting up the transfer @@ -545,6 +547,158 @@ contract TestPerChainThresholds is Test, IRateLimiterEvents { require(token1.balanceOf(userD) == sendingAmount, "User received funds"); } + // This test does a transfer between chain one and chain three where there there is only one transceiver and the per-chain threshold is one. + // The transfer is not completed until both VAAs are posted. + function test_perChainTransceiver() public { + // By default, there are two transceivers and a threshold of two. + nttManagerChain1.setTransceiver(address(wormholeTransceiverChain1)); + nttManagerChain1.setTransceiver(address(secondWormholeTransceiverChain1)); + nttManagerChain2.setTransceiver(address(wormholeTransceiverChain2)); + nttManagerChain2.setTransceiver(address(secondWormholeTransceiverChain2)); + nttManagerChain3.setTransceiver(address(wormholeTransceiverChain3)); + nttManagerChain3.setTransceiver(address(secondWormholeTransceiverChain3)); + + nttManagerChain1.setThreshold(2); + nttManagerChain2.setThreshold(2); + nttManagerChain3.setThreshold(2); + + // Between chains one and three, there is only a single transceiver with a threshold of one. + nttManagerChain1.enableTransceiverForChain(address(wormholeTransceiverChain1), chainId3); + nttManagerChain3.enableTransceiverForChain(address(wormholeTransceiverChain3), chainId1); + nttManagerChain1.setThresholdPerChain(chainId3, 1); + nttManagerChain3.setThresholdPerChain(chainId1, 1); + + require(nttManagerChain1.getThreshold() == 2, "Default threshold is wrong on chain 1"); + require(nttManagerChain1.getThreshold(chainId3) == 1, "Threshold for chain 3 is wrong on chain 1"); + require(nttManagerChain1.getThreshold() == 2, "Default threshold is wrong on chain 3"); + require(nttManagerChain3.getThreshold(chainId1) == 1, "Threshold for chain 1 is wrong on chain 3"); + + vm.chainId(chainId1); + + // Setting up the transfer + DummyToken token1 = DummyToken(nttManagerChain1.token()); + DummyToken token3 = DummyTokenMintAndBurn(nttManagerChain3.token()); + + uint8 decimals = token1.decimals(); + uint256 sendingAmount = 5 * 10 ** decimals; + token1.mintDummy(address(userA), 5 * 10 ** decimals); + vm.startPrank(userA); + token1.approve(address(nttManagerChain1), sendingAmount); + + vm.recordLogs(); + + // Send token from chain 1 to chain 3, userB. + { + uint256 nttManagerBalanceBefore = token1.balanceOf(address(nttManagerChain1)); + uint256 userBalanceBefore = token1.balanceOf(address(userA)); + nttManagerChain1.transfer(sendingAmount, chainId3, bytes32(uint256(uint160(userB)))); + + // Balance check on funds going in and out working as expected + uint256 nttManagerBalanceAfter = token1.balanceOf(address(nttManagerChain1)); + uint256 userBalanceAfter = token1.balanceOf(address(userB)); + require( + nttManagerBalanceBefore + sendingAmount == nttManagerBalanceAfter, + "Should be locking the tokens" + ); + require( + userBalanceBefore - sendingAmount == userBalanceAfter, + "User should have sent tokens" + ); + } + + vm.stopPrank(); + + // Get and sign the log to go down the other pipes. There should be two messages since we have two transceivers. + Vm.Log[] memory entries = guardian.fetchWormholeMessageFromLog(vm.getRecordedLogs()); + require(2 == entries.length, "Unexpected number of log entries 3"); + bytes[] memory encodedVMs = new bytes[](entries.length); + for (uint256 i = 0; i < encodedVMs.length; i++) { + encodedVMs[i] = guardian.fetchSignedMessageFromLogs(entries[i], chainId1); + } + + // Chain3 verification and checks + vm.chainId(chainId3); + + uint256 supplyBefore = token3.totalSupply(); +/* + // Submit the first message on chain 3. The numbers shouldn't change yet since the threshold is two. + wormholeTransceiverChain3.receiveMessage(encodedVMs[0]); + uint256 supplyAfter = token3.totalSupply(); + + require(supplyBefore == supplyAfter, "Supplies changed early"); + require(token3.balanceOf(userB) == 0, "User receive tokens early"); + require(token3.balanceOf(address(nttManagerChain3)) == 0, "NttManager has unintended funds"); + + // Submit the second message and the transfer should complete. + secondWormholeTransceiverChain3.receiveMessage(encodedVMs[1]); + supplyAfter = token3.totalSupply(); + + require(sendingAmount + supplyBefore == supplyAfter, "Supplies dont match"); + require(token3.balanceOf(userB) == sendingAmount, "User didn't receive tokens"); + require(token3.balanceOf(address(nttManagerChain3)) == 0, "NttManager has unintended funds"); + + // Go back the other way from a THIRD user + vm.prank(userB); + token3.transfer(userC, sendingAmount); + + vm.startPrank(userC); + token3.approve(address(nttManagerChain3), sendingAmount); + vm.recordLogs(); + + // Supply checks on the transfer + supplyBefore = token3.totalSupply(); + nttManagerChain3.transfer( + sendingAmount, + chainId1, + toWormholeFormat(userD), + toWormholeFormat(userC), + false, + encodeTransceiverInstruction(true) + ); + + supplyAfter = token3.totalSupply(); + + require(sendingAmount - supplyBefore == supplyAfter, "Supplies don't match"); + require(token3.balanceOf(userB) == 0, "OG user receive tokens"); + require(token3.balanceOf(userC) == 0, "Sending user didn't receive tokens"); + require( + token3.balanceOf(address(nttManagerChain3)) == 0, + "NttManager didn't receive unintended funds" + ); + + // Get and sign the log to go down the other pipe. Thank you to whoever wrote this code in the past! + entries = guardian.fetchWormholeMessageFromLog(vm.getRecordedLogs()); + require(2 == entries.length, "Unexpected number of log entries for response"); + encodedVMs = new bytes[](entries.length); + for (uint256 i = 0; i < encodedVMs.length; i++) { + encodedVMs[i] = guardian.fetchSignedMessageFromLogs(entries[i], chainId3); + } + + // Chain1 verification and checks with the receiving of the message + vm.chainId(chainId1); + + // Submit the first message back on chain one. Nothing should happen because our threshold is two. + supplyBefore = token1.totalSupply(); + wormholeTransceiverChain1.receiveMessage(encodedVMs[0]); + supplyAfter = token1.totalSupply(); + + require(supplyBefore == supplyAfter, "Supplies don't match between operations"); + require(token1.balanceOf(userB) == 0, "OG user receive tokens"); + require(token1.balanceOf(userC) == 0, "Sending user didn't receive tokens"); + require(token1.balanceOf(userD) == 0, "User received funds before they should"); + + // Submit the second message back on chain one. This should update the balance. + supplyBefore = token1.totalSupply(); + secondWormholeTransceiverChain1.receiveMessage(encodedVMs[1]); + supplyAfter = token1.totalSupply(); + + require(supplyBefore == supplyAfter, "Supplies don't match between operations"); /////////////////// Is this right?? + require(token1.balanceOf(userB) == 0, "OG user receive tokens"); + require(token1.balanceOf(userC) == 0, "Sending user didn't receive tokens"); + require(token1.balanceOf(userD) == sendingAmount, "User received funds"); + */ + } + function encodeTransceiverInstruction(bool relayer_off) public view returns (bytes memory) { WormholeTransceiver.WormholeTransceiverInstruction memory instruction = IWormholeTransceiver.WormholeTransceiverInstruction(relayer_off); diff --git a/evm/test/IntegrationPerChainTransceivers.t.sol b/evm/test/IntegrationPerChainTransceivers.t.sol new file mode 100755 index 000000000..ab8ff8fe7 --- /dev/null +++ b/evm/test/IntegrationPerChainTransceivers.t.sol @@ -0,0 +1,582 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity >=0.8.8 <0.9.0; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +import "../src/NttManager/NttManager.sol"; +import "../src/Transceiver/Transceiver.sol"; +import "../src/interfaces/INttManager.sol"; +import "../src/interfaces/IRateLimiter.sol"; +import "../src/interfaces/ITransceiver.sol"; +import "../src/interfaces/IManagerBase.sol"; +import "../src/interfaces/IRateLimiterEvents.sol"; +import {Utils} from "./libraries/Utils.sol"; +import {DummyToken, DummyTokenMintAndBurn} from "./NttManager.t.sol"; +import "../src/interfaces/IWormholeTransceiver.sol"; +import {WormholeTransceiver} from "../src/Transceiver/WormholeTransceiver/WormholeTransceiver.sol"; +import "../src/libraries/TransceiverStructs.sol"; +import "./mocks/MockNttManager.sol"; +import "./mocks/MockTransceivers.sol"; + +import "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; +import "openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import "wormhole-solidity-sdk/interfaces/IWormhole.sol"; +import "wormhole-solidity-sdk/testing/helpers/WormholeSimulator.sol"; +import "wormhole-solidity-sdk/Utils.sol"; +//import "wormhole-solidity-sdk/testing/WormholeRelayerTest.sol"; + +contract TestPerChainThresholds is Test, IRateLimiterEvents { + NttManager nttManagerChain1; + NttManager nttManagerChain2; + NttManager nttManagerChain3; + + using TrimmedAmountLib for uint256; + using TrimmedAmountLib for TrimmedAmount; + + uint16 constant chainId1 = 7; + uint16 constant chainId2 = 100; + uint16 constant chainId3 = 101; + uint8 constant FAST_CONSISTENCY_LEVEL = 200; + uint256 constant GAS_LIMIT = 500000; + + uint16 constant SENDING_CHAIN_ID = 1; + uint256 constant DEVNET_GUARDIAN_PK = + 0xcfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0; + WormholeSimulator guardian; + uint256 initialBlockTimestamp; + + WormholeTransceiver wormholeTransceiverChain1; + WormholeTransceiver secondWormholeTransceiverChain1; + WormholeTransceiver wormholeTransceiverChain2; + WormholeTransceiver secondWormholeTransceiverChain2; + WormholeTransceiver wormholeTransceiverChain3; + WormholeTransceiver secondWormholeTransceiverChain3; + address userA = address(0x123); + address userB = address(0x456); + address userC = address(0x789); + address userD = address(0xABC); + + address relayer = address(0x28D8F1Be96f97C1387e94A53e00eCcFb4E75175a); + IWormhole wormhole = IWormhole(0x4a8bc80Ed5a4067f1CCf107057b8270E0cC11A78); + + // This function sets up the following config: + // - A manager on each of three chains. + // - Two transceivers on each chain, all interconnected as peers. + // - On chain one, it sets a default threshold of one and a per-chain threshold of two for chain three. + // - On chain three, it sets a default threshold of one and a per-chain threshold of two for chain one. + + function setUp() public { + string memory url = "https://ethereum-sepolia-rpc.publicnode.com"; + vm.createSelectFork(url); + initialBlockTimestamp = vm.getBlockTimestamp(); + + guardian = new WormholeSimulator(address(wormhole), DEVNET_GUARDIAN_PK); + + vm.chainId(chainId1); + DummyToken t1 = new DummyToken(); + NttManager implementation = new MockNttManagerContract( + address(t1), IManagerBase.Mode.LOCKING, chainId1, 1 days, false + ); + + nttManagerChain1 = + MockNttManagerContract(address(new ERC1967Proxy(address(implementation), ""))); + nttManagerChain1.initialize(); + + // Create the first transceiver, from chain 1 to chain 2. + WormholeTransceiver wormholeTransceiverChain1Implementation = new MockWormholeTransceiverContract( + address(nttManagerChain1), + address(wormhole), + address(relayer), + address(0x0), + FAST_CONSISTENCY_LEVEL, + GAS_LIMIT + ); + wormholeTransceiverChain1 = MockWormholeTransceiverContract( + address(new ERC1967Proxy(address(wormholeTransceiverChain1Implementation), "")) + ); + + // Only the deployer should be able to initialize + vm.prank(userA); + vm.expectRevert( + abi.encodeWithSelector(ITransceiver.UnexpectedDeployer.selector, address(this), userA) + ); + wormholeTransceiverChain1.initialize(); + + // Actually initialize properly now + wormholeTransceiverChain1.initialize(); + + nttManagerChain1.setTransceiver(address(wormholeTransceiverChain1)); + nttManagerChain1.setOutboundLimit(type(uint64).max); + nttManagerChain1.setInboundLimit(type(uint64).max, chainId2); + nttManagerChain1.setInboundLimit(type(uint64).max, chainId3); + + // Create the second transceiver for chain 1. + WormholeTransceiver secondWormholeTransceiverChain1Implementation = new MockWormholeTransceiverContract( + address(nttManagerChain1), + address(wormhole), + address(relayer), + address(0x0), + FAST_CONSISTENCY_LEVEL, + GAS_LIMIT + ); + secondWormholeTransceiverChain1 = MockWormholeTransceiverContract( + address(new ERC1967Proxy(address(secondWormholeTransceiverChain1Implementation), "")) + ); + + secondWormholeTransceiverChain1.initialize(); + nttManagerChain1.setTransceiver(address(secondWormholeTransceiverChain1)); + + // Chain 2 setup + vm.chainId(chainId2); + DummyToken t2 = new DummyTokenMintAndBurn(); + NttManager implementationChain2 = new MockNttManagerContract( + address(t2), IManagerBase.Mode.BURNING, chainId2, 1 days, false + ); + + nttManagerChain2 = + MockNttManagerContract(address(new ERC1967Proxy(address(implementationChain2), ""))); + nttManagerChain2.initialize(); + + WormholeTransceiver wormholeTransceiverChain2Implementation = new MockWormholeTransceiverContract( + address(nttManagerChain2), + address(wormhole), + address(relayer), + address(0x0), + FAST_CONSISTENCY_LEVEL, + GAS_LIMIT + ); + wormholeTransceiverChain2 = MockWormholeTransceiverContract( + address(new ERC1967Proxy(address(wormholeTransceiverChain2Implementation), "")) + ); + wormholeTransceiverChain2.initialize(); + + nttManagerChain2.setTransceiver(address(wormholeTransceiverChain2)); + nttManagerChain2.setOutboundLimit(type(uint64).max); + nttManagerChain2.setInboundLimit(type(uint64).max, chainId1); + + // Register peer contracts for the nttManager and transceiver. Transceivers and nttManager each have the concept of peers here. + nttManagerChain1.setPeer( + chainId2, bytes32(uint256(uint160(address(nttManagerChain2)))), 9, type(uint64).max + ); + nttManagerChain2.setPeer( + chainId1, bytes32(uint256(uint160(address(nttManagerChain1)))), 7, type(uint64).max + ); + + // Create the second transceiver for chain 2. + WormholeTransceiver secondWormholeTransceiverChain2Implementation = new MockWormholeTransceiverContract( + address(nttManagerChain2), + address(wormhole), + address(relayer), + address(0x0), + FAST_CONSISTENCY_LEVEL, + GAS_LIMIT + ); + secondWormholeTransceiverChain2 = MockWormholeTransceiverContract( + address(new ERC1967Proxy(address(secondWormholeTransceiverChain2Implementation), "")) + ); + + secondWormholeTransceiverChain2.initialize(); + nttManagerChain2.setTransceiver(address(secondWormholeTransceiverChain2)); + + // Set peers for the transceivers + wormholeTransceiverChain1.setWormholePeer( + chainId2, bytes32(uint256(uint160(address(wormholeTransceiverChain2)))) + ); + wormholeTransceiverChain2.setWormholePeer( + chainId1, bytes32(uint256(uint160(address(wormholeTransceiverChain1)))) + ); + secondWormholeTransceiverChain1.setWormholePeer( + chainId2, bytes32(uint256(uint160(address(secondWormholeTransceiverChain2)))) + ); + secondWormholeTransceiverChain2.setWormholePeer( + chainId1, bytes32(uint256(uint160(address(secondWormholeTransceiverChain1)))) + ); + + // Chain 3 setup + vm.chainId(chainId3); + DummyToken t3 = new DummyTokenMintAndBurn(); + NttManager implementationChain3 = new MockNttManagerContract( + address(t3), IManagerBase.Mode.BURNING, chainId3, 1 days, false + ); + + nttManagerChain3 = + MockNttManagerContract(address(new ERC1967Proxy(address(implementationChain3), ""))); + nttManagerChain3.initialize(); + + WormholeTransceiver wormholeTransceiverChain3Implementation = new MockWormholeTransceiverContract( + address(nttManagerChain3), + address(wormhole), + address(relayer), + address(0x0), + FAST_CONSISTENCY_LEVEL, + GAS_LIMIT + ); + wormholeTransceiverChain3 = MockWormholeTransceiverContract( + address(new ERC1967Proxy(address(wormholeTransceiverChain3Implementation), "")) + ); + wormholeTransceiverChain3.initialize(); + + nttManagerChain3.setTransceiver(address(wormholeTransceiverChain3)); + nttManagerChain3.setOutboundLimit(type(uint64).max); + nttManagerChain3.setInboundLimit(type(uint64).max, chainId1); + + // Register peer contracts for the nttManager and transceiver. Transceivers and nttManager each have the concept of peers here. + nttManagerChain1.setPeer( + chainId3, bytes32(uint256(uint160(address(nttManagerChain3)))), 9, type(uint64).max + ); + nttManagerChain3.setPeer( + chainId1, bytes32(uint256(uint160(address(nttManagerChain1)))), 7, type(uint64).max + ); + + // Create the second transceiver, from chain 3 to chain 1. + WormholeTransceiver secondWormholeTransceiverChain3Implementation = new MockWormholeTransceiverContract( + address(nttManagerChain3), + address(wormhole), + address(relayer), + address(0x0), + FAST_CONSISTENCY_LEVEL, + GAS_LIMIT + ); + secondWormholeTransceiverChain3 = MockWormholeTransceiverContract( + address(new ERC1967Proxy(address(secondWormholeTransceiverChain3Implementation), "")) + ); + + // Actually initialize properly now + secondWormholeTransceiverChain3.initialize(); + + nttManagerChain3.setTransceiver(address(secondWormholeTransceiverChain3)); + nttManagerChain1.setOutboundLimit(type(uint64).max); + nttManagerChain1.setInboundLimit(type(uint64).max, chainId3); + + // Set peers for the transceivers + wormholeTransceiverChain1.setWormholePeer( + chainId3, bytes32(uint256(uint160(address(wormholeTransceiverChain3)))) + ); + wormholeTransceiverChain3.setWormholePeer( + chainId1, bytes32(uint256(uint160(address(wormholeTransceiverChain1)))) + ); + wormholeTransceiverChain3.setWormholePeer( + chainId2, bytes32(uint256(uint160(address(wormholeTransceiverChain2)))) + ); + wormholeTransceiverChain2.setWormholePeer( + chainId3, bytes32(uint256(uint160(address(wormholeTransceiverChain3)))) + ); + secondWormholeTransceiverChain1.setWormholePeer( + chainId3, bytes32(uint256(uint160(address(secondWormholeTransceiverChain3)))) + ); + secondWormholeTransceiverChain3.setWormholePeer( + chainId1, bytes32(uint256(uint160(address(secondWormholeTransceiverChain1)))) + ); + secondWormholeTransceiverChain2.setWormholePeer( + chainId3, bytes32(uint256(uint160(address(secondWormholeTransceiverChain3)))) + ); + secondWormholeTransceiverChain3.setWormholePeer( + chainId2, bytes32(uint256(uint160(address(secondWormholeTransceiverChain2)))) + ); + + // Set the thresholds. + require(nttManagerChain1.getThreshold() != 0, "Threshold is zero with active transceivers"); + + // Actually set it + nttManagerChain1.setThreshold(1); + nttManagerChain2.setThreshold(1); + nttManagerChain3.setThreshold(1); + + // On chain 1, set the threshold to chain 3 to be different. + nttManagerChain1.setThresholdPerChain(chainId3, 2); + nttManagerChain3.setThresholdPerChain(chainId1, 2); + + require(nttManagerChain1.getThreshold() == 1, "Default threshold is wrong"); + require(nttManagerChain1.getThreshold(chainId3) == 2, "Threshold for chain 3 is wrong"); + } + + // This test does a transfer between chain one and chain two. + // Since chain two uses the default threshold, posting a VAA from only one transceiver completes the transfer. + function test_defaultThreshold() public { + vm.chainId(chainId1); + + // Setting up the transfer + DummyToken token1 = DummyToken(nttManagerChain1.token()); + DummyToken token2 = DummyTokenMintAndBurn(nttManagerChain2.token()); + + uint8 decimals = token1.decimals(); + uint256 sendingAmount = 5 * 10 ** decimals; + token1.mintDummy(address(userA), 5 * 10 ** decimals); + + // Transfer tokens from chain one to chain two through standard means (not relayer) + vm.startPrank(userA); + token1.approve(address(nttManagerChain1), sendingAmount); + vm.recordLogs(); + { + uint256 nttManagerBalanceBefore = token1.balanceOf(address(nttManagerChain1)); + uint256 userBalanceBefore = token1.balanceOf(address(userA)); + nttManagerChain1.transfer(sendingAmount, chainId2, bytes32(uint256(uint160(userB)))); + + // Balance check on funds going in and out working as expected + uint256 nttManagerBalanceAfter = token1.balanceOf(address(nttManagerChain1)); + uint256 userBalanceAfter = token1.balanceOf(address(userB)); + require( + nttManagerBalanceBefore + sendingAmount == nttManagerBalanceAfter, + "Should be locking the tokens" + ); + require( + userBalanceBefore - sendingAmount == userBalanceAfter, + "User should have sent tokens" + ); + } + + vm.stopPrank(); + + // Get and sign the log to go down the other pipes. There should be two messages since we have two transceivers. + Vm.Log[] memory entries = guardian.fetchWormholeMessageFromLog(vm.getRecordedLogs()); + require(2 == entries.length, "Unexpected number of log entries 1"); + bytes[] memory encodedVMs = new bytes[](entries.length); + for (uint256 i = 0; i < encodedVMs.length; i++) { + encodedVMs[i] = guardian.fetchSignedMessageFromLogs(entries[i], chainId1); + } + + // Chain2 verification and checks + vm.chainId(chainId2); + + { + uint256 supplyBefore = token2.totalSupply(); + wormholeTransceiverChain2.receiveMessage(encodedVMs[0]); + uint256 supplyAfter = token2.totalSupply(); + + require(sendingAmount + supplyBefore == supplyAfter, "Supplies dont match"); + require(token2.balanceOf(userB) == sendingAmount, "User didn't receive tokens"); + require( + token2.balanceOf(address(nttManagerChain2)) == 0, "NttManager has unintended funds" + ); + } + + // Go back the other way from a THIRD user + vm.prank(userB); + token2.transfer(userC, sendingAmount); + + vm.startPrank(userC); + token2.approve(address(nttManagerChain2), sendingAmount); + vm.recordLogs(); + + // Supply checks on the transfer + { + uint256 supplyBefore = token2.totalSupply(); + nttManagerChain2.transfer( + sendingAmount, + chainId1, + toWormholeFormat(userD), + toWormholeFormat(userC), + false, + encodeTransceiverInstruction(true) + ); + + uint256 supplyAfter = token2.totalSupply(); + + require(sendingAmount - supplyBefore == supplyAfter, "Supplies don't match"); + require(token2.balanceOf(userB) == 0, "OG user receive tokens"); + require(token2.balanceOf(userC) == 0, "Sending user didn't receive tokens"); + require( + token2.balanceOf(address(nttManagerChain2)) == 0, + "NttManager didn't receive unintended funds" + ); + } + + // Get and sign the log to go down the other pipe. Thank you to whoever wrote this code in the past! + entries = guardian.fetchWormholeMessageFromLog(vm.getRecordedLogs()); + require(2 == entries.length, "Unexpected number of log entries 2"); + encodedVMs = new bytes[](entries.length); + for (uint256 i = 0; i < encodedVMs.length; i++) { + encodedVMs[i] = guardian.fetchSignedMessageFromLogs(entries[i], chainId2); + } + + vm.chainId(chainId1); + + // Receive the first message. Since the threshold is set to one, the transfer should complete. + { + uint256 supplyBefore = token1.totalSupply(); + wormholeTransceiverChain1.receiveMessage(encodedVMs[0]); + uint256 supplyAfter = token1.totalSupply(); + + require(supplyBefore == supplyAfter, "Supplies don't match between operations"); + require(token1.balanceOf(userB) == 0, "OG user receive tokens"); + require(token1.balanceOf(userC) == 0, "Sending user didn't receive tokens"); + require(token1.balanceOf(userD) == sendingAmount, "User received funds"); + } + + // Receive the second message. That should do nothing since we've already processed the transfer. + { + uint256 supplyBefore = token1.totalSupply(); + secondWormholeTransceiverChain1.receiveMessage(encodedVMs[1]); + uint256 supplyAfter = token1.totalSupply(); + + require(supplyBefore == supplyAfter, "Supplies don't match between operations"); + require(token1.balanceOf(userB) == 0, "OG user receive tokens"); + require(token1.balanceOf(userC) == 0, "Sending user didn't receive tokens"); + require(token1.balanceOf(userD) == sendingAmount, "User received funds"); + } + } + + // This test does a transfer between chain one and chain three. + // Since the threshold for these two chains is two, the transfer is not completed until both VAAs are posted. + function test_perChainThreshold() public { + vm.chainId(chainId1); + + // Setting up the transfer + DummyToken token1 = DummyToken(nttManagerChain1.token()); + DummyToken token3 = DummyTokenMintAndBurn(nttManagerChain3.token()); + + uint8 decimals = token1.decimals(); + uint256 sendingAmount = 5 * 10 ** decimals; + token1.mintDummy(address(userA), 5 * 10 ** decimals); + vm.startPrank(userA); + token1.approve(address(nttManagerChain1), sendingAmount); + + vm.recordLogs(); + + // Send token from chain 1 to chain 3, userB. + { + uint256 nttManagerBalanceBefore = token1.balanceOf(address(nttManagerChain1)); + uint256 userBalanceBefore = token1.balanceOf(address(userA)); + nttManagerChain1.transfer(sendingAmount, chainId3, bytes32(uint256(uint160(userB)))); + + // Balance check on funds going in and out working as expected + uint256 nttManagerBalanceAfter = token1.balanceOf(address(nttManagerChain1)); + uint256 userBalanceAfter = token1.balanceOf(address(userB)); + require( + nttManagerBalanceBefore + sendingAmount == nttManagerBalanceAfter, + "Should be locking the tokens" + ); + require( + userBalanceBefore - sendingAmount == userBalanceAfter, + "User should have sent tokens" + ); + } + + vm.stopPrank(); + + // Get and sign the log to go down the other pipes. There should be two messages since we have two transceivers. + Vm.Log[] memory entries = guardian.fetchWormholeMessageFromLog(vm.getRecordedLogs()); + require(2 == entries.length, "Unexpected number of log entries 3"); + bytes[] memory encodedVMs = new bytes[](entries.length); + for (uint256 i = 0; i < encodedVMs.length; i++) { + encodedVMs[i] = guardian.fetchSignedMessageFromLogs(entries[i], chainId1); + } + + // Chain3 verification and checks + vm.chainId(chainId3); + + uint256 supplyBefore = token3.totalSupply(); + + // Submit the first message on chain 3. The numbers shouldn't change yet since the threshold is two. + wormholeTransceiverChain3.receiveMessage(encodedVMs[0]); + uint256 supplyAfter = token3.totalSupply(); + + require(supplyBefore == supplyAfter, "Supplies changed early"); + require(token3.balanceOf(userB) == 0, "User receive tokens early"); + require(token3.balanceOf(address(nttManagerChain3)) == 0, "NttManager has unintended funds"); + + // Submit the second message and the transfer should complete. + secondWormholeTransceiverChain3.receiveMessage(encodedVMs[1]); + supplyAfter = token3.totalSupply(); + + require(sendingAmount + supplyBefore == supplyAfter, "Supplies dont match"); + require(token3.balanceOf(userB) == sendingAmount, "User didn't receive tokens"); + require(token3.balanceOf(address(nttManagerChain3)) == 0, "NttManager has unintended funds"); + + // Go back the other way from a THIRD user + vm.prank(userB); + token3.transfer(userC, sendingAmount); + + vm.startPrank(userC); + token3.approve(address(nttManagerChain3), sendingAmount); + vm.recordLogs(); + + // Supply checks on the transfer + supplyBefore = token3.totalSupply(); + nttManagerChain3.transfer( + sendingAmount, + chainId1, + toWormholeFormat(userD), + toWormholeFormat(userC), + false, + encodeTransceiverInstruction(true) + ); + + supplyAfter = token3.totalSupply(); + + require(sendingAmount - supplyBefore == supplyAfter, "Supplies don't match"); + require(token3.balanceOf(userB) == 0, "OG user receive tokens"); + require(token3.balanceOf(userC) == 0, "Sending user didn't receive tokens"); + require( + token3.balanceOf(address(nttManagerChain3)) == 0, + "NttManager didn't receive unintended funds" + ); + + // Get and sign the log to go down the other pipe. Thank you to whoever wrote this code in the past! + entries = guardian.fetchWormholeMessageFromLog(vm.getRecordedLogs()); + require(2 == entries.length, "Unexpected number of log entries for response"); + encodedVMs = new bytes[](entries.length); + for (uint256 i = 0; i < encodedVMs.length; i++) { + encodedVMs[i] = guardian.fetchSignedMessageFromLogs(entries[i], chainId3); + } + + // Chain1 verification and checks with the receiving of the message + vm.chainId(chainId1); + + // Submit the first message back on chain one. Nothing should happen because our threshold is two. + supplyBefore = token1.totalSupply(); + wormholeTransceiverChain1.receiveMessage(encodedVMs[0]); + supplyAfter = token1.totalSupply(); + + require(supplyBefore == supplyAfter, "Supplies don't match between operations"); + require(token1.balanceOf(userB) == 0, "OG user receive tokens"); + require(token1.balanceOf(userC) == 0, "Sending user didn't receive tokens"); + require(token1.balanceOf(userD) == 0, "User received funds before they should"); + + // Submit the second message back on chain one. This should update the balance. + supplyBefore = token1.totalSupply(); + secondWormholeTransceiverChain1.receiveMessage(encodedVMs[1]); + supplyAfter = token1.totalSupply(); + + require(supplyBefore == supplyAfter, "Supplies don't match between operations"); /////////////////// Is this right?? + require(token1.balanceOf(userB) == 0, "OG user receive tokens"); + require(token1.balanceOf(userC) == 0, "Sending user didn't receive tokens"); + require(token1.balanceOf(userD) == sendingAmount, "User received funds"); + } + + function encodeTransceiverInstruction(bool relayer_off) public view returns (bytes memory) { + WormholeTransceiver.WormholeTransceiverInstruction memory instruction = + IWormholeTransceiver.WormholeTransceiverInstruction(relayer_off); + bytes memory encodedInstructionWormhole = + wormholeTransceiverChain1.encodeWormholeTransceiverInstruction(instruction); + TransceiverStructs.TransceiverInstruction memory TransceiverInstruction = TransceiverStructs + .TransceiverInstruction({index: 0, payload: encodedInstructionWormhole}); + TransceiverStructs.TransceiverInstruction[] memory TransceiverInstructions = + new TransceiverStructs.TransceiverInstruction[](1); + TransceiverInstructions[0] = TransceiverInstruction; + return TransceiverStructs.encodeTransceiverInstructions(TransceiverInstructions); + } + + // Encode an instruction for each of the relayers + function encodeTransceiverInstructions(bool relayer_off) public view returns (bytes memory) { + WormholeTransceiver.WormholeTransceiverInstruction memory instruction = + IWormholeTransceiver.WormholeTransceiverInstruction(relayer_off); + + bytes memory encodedInstructionWormhole = + wormholeTransceiverChain1.encodeWormholeTransceiverInstruction(instruction); + + TransceiverStructs.TransceiverInstruction memory TransceiverInstruction1 = + TransceiverStructs.TransceiverInstruction({index: 0, payload: encodedInstructionWormhole}); + TransceiverStructs.TransceiverInstruction memory TransceiverInstruction2 = + TransceiverStructs.TransceiverInstruction({index: 1, payload: encodedInstructionWormhole}); + + TransceiverStructs.TransceiverInstruction[] memory TransceiverInstructions = + new TransceiverStructs.TransceiverInstruction[](2); + + TransceiverInstructions[0] = TransceiverInstruction1; + TransceiverInstructions[1] = TransceiverInstruction2; + + return TransceiverStructs.encodeTransceiverInstructions(TransceiverInstructions); + } +} From e9a2d469d088c362769dfd6ca660091bcfc7ae97 Mon Sep 17 00:00:00 2001 From: Bruce Riley Date: Mon, 23 Sep 2024 21:57:01 -0500 Subject: [PATCH 04/10] EVM: Rework per-chain transceivers --- evm/src/NttManager/ManagerBase.sol | 101 ++- .../NttManager/NttManagerNoRateLimiting.sol | 6 +- .../NttManagerWithPerChainTransceivers.sol | 150 ++++ evm/src/NttManager/TransceiverRegistry.sol | 51 -- evm/src/interfaces/IManagerBase.sol | 36 +- evm/src/interfaces/INttManager.sol | 3 - evm/test/IntegrationPerChainThresholds.t.sol | 736 ------------------ evm/test/IntegrationWithoutRateLimiting.t.sol | 11 +- evm/test/NttManagerNoRateLimiting.t.sol | 6 +- ...ivers.t.sol => PerChainTransceivers.t.sol} | 149 +++- evm/test/PerChainTransceiversDemo.t.sol | 430 ++++++++++ evm/test/mocks/MockNttManager.sol | 21 + 12 files changed, 792 insertions(+), 908 deletions(-) create mode 100644 evm/src/NttManager/NttManagerWithPerChainTransceivers.sol delete mode 100644 evm/test/IntegrationPerChainThresholds.t.sol rename evm/test/{IntegrationPerChainTransceivers.t.sol => PerChainTransceivers.t.sol} (82%) create mode 100755 evm/test/PerChainTransceiversDemo.t.sol diff --git a/evm/src/NttManager/ManagerBase.sol b/evm/src/NttManager/ManagerBase.sol index 39522fe70..a269660b3 100644 --- a/evm/src/NttManager/ManagerBase.sol +++ b/evm/src/NttManager/ManagerBase.sol @@ -64,24 +64,11 @@ abstract contract ManagerBase is bytes32 private constant MESSAGE_SEQUENCE_SLOT = bytes32(uint256(keccak256("ntt.messageSequence")) - 1); - bytes32 private constant THRESHOLD_SLOT = bytes32(uint256(keccak256("ntt.threshold")) - 1); + bytes32 internal constant THRESHOLD_SLOT = bytes32(uint256(keccak256("ntt.threshold")) - 1); // =============== Storage Getters/Setters ============================================== - function _getThresholdStorage() private pure returns (_Threshold storage $) { - uint256 slot = uint256(THRESHOLD_SLOT); - assembly ("memory-safe") { - $.slot := slot - } - } - - function _getThresholdStoragePerChain() - private - pure - returns (mapping(uint16 => _Threshold) storage $) - { - // TODO: this is safe (reusing the storage slot, because the mapping - // doesn't write into the slot itself) buy maybe we shouldn't? + function _getThresholdStorage() internal pure returns (_Threshold storage $) { uint256 slot = uint256(THRESHOLD_SLOT); assembly ("memory-safe") { $.slot := slot @@ -135,7 +122,7 @@ abstract contract ManagerBase is uint256 totalPriceQuote = 0; for (uint256 i = 0; i < numEnabledTransceivers; i++) { address transceiverAddr = enabledTransceivers[i]; - if (!_isTransceiverEnabledForChain(transceiverAddr, recipientChain)) { + if (!_isSendTransceiverEnabledForChain(transceiverAddr, recipientChain)) { continue; } uint8 registeredTransceiverIndex = transceiverInfos[transceiverAddr].index; @@ -217,7 +204,7 @@ abstract contract ManagerBase is // call into transceiver contracts to send the message for (uint256 i = 0; i < numEnabledTransceivers; i++) { address transceiverAddr = enabledTransceivers[i]; - if (!_isTransceiverEnabledForChain(transceiverAddr, recipientChain)) { + if (!_isSendTransceiverEnabledForChain(transceiverAddr, recipientChain)) { continue; } @@ -304,14 +291,11 @@ abstract contract ManagerBase is return _getThresholdStorage().num; } - function getThreshold( - uint16 forChainId - ) public view returns (uint8) { - uint8 threshold = _getThresholdStoragePerChain()[forChainId].num; - if (threshold == 0) { - return _getThresholdStorage().num; - } - return threshold; + /// @inheritdoc IManagerBase + function getPerChainThreshold( + uint16 // forChainId + ) public view virtual returns (uint8) { + return _getThresholdStorage().num; } /// @inheritdoc IManagerBase @@ -319,7 +303,7 @@ abstract contract ManagerBase is bytes32 digest ) public view returns (bool) { uint16 sourceChainId = _getMessageAttestationsStorage()[digest].sourceChainId; - uint8 threshold = getThreshold(sourceChainId); + uint8 threshold = getPerChainThreshold(sourceChainId); return messageAttestations(digest) >= threshold && threshold > 0; } @@ -379,15 +363,6 @@ abstract contract ManagerBase is ITransceiver(_registeredTransceivers[i]).transferTransceiverOwnership(newOwner); } } - - /// @inheritdoc IManagerBase - function enableTransceiverForChain( - address transceiver, - uint16 forChainId - ) external onlyOwner { - _enableTransceiverForChain(transceiver, forChainId); - emit TransceiverEnabledForChain(transceiver, forChainId); - } /// @inheritdoc IManagerBase function setTransceiver( @@ -436,6 +411,22 @@ abstract contract ManagerBase is _checkThresholdInvariants(); } + /// @inheritdoc IManagerBase + function enableSendTransceiverForChain( + address, // transceiver, + uint16 // forChainId + ) external virtual onlyOwner { + revert NotImplemented(); + } + + /// @inheritdoc IManagerBase + function enableRecvTransceiverForChain( + address, // transceiver, + uint16 // forChainId + ) external virtual onlyOwner { + revert NotImplemented(); + } + /// @inheritdoc IManagerBase function setThreshold( uint8 threshold @@ -452,20 +443,13 @@ abstract contract ManagerBase is emit ThresholdChanged(oldThreshold, threshold); } - - /// @inheritdoc IManagerBase - function setThresholdPerChain(uint16 forChainId, uint8 threshold) external onlyOwner { - if (threshold == 0) { - revert ZeroThreshold(); - } - - mapping(uint16 => _Threshold) storage _threshold = _getThresholdStoragePerChain(); - uint8 oldThreshold = _threshold[forChainId].num; - _threshold[forChainId].num = threshold; - _checkThresholdInvariants(_threshold[forChainId].num); - - emit PerChainThresholdChanged(forChainId, oldThreshold, threshold); + /// @inheritdoc IManagerBase + function setPerChainThreshold( + uint16, // forChainId, + uint8 // threshold + ) external virtual onlyOwner { + revert NotImplemented(); } // =============== Internal ============================================================== @@ -488,7 +472,7 @@ abstract contract ManagerBase is ) internal view returns (uint64) { uint64 enabledTransceiverBitmap = _getEnabledTransceiversBitmap(); uint16 sourceChainId = _getMessageAttestationsStorage()[digest].sourceChainId; - uint64 enabledTransceiversForChain = _getEnabledTransceiversBitmapForChain(sourceChainId); + uint64 enabledTransceiversForChain = _getEnabledRecvTransceiversForChain(sourceChainId); return _getMessageAttestationsStorage()[digest].attestedTransceivers & enabledTransceiverBitmap & enabledTransceiversForChain; } @@ -521,6 +505,19 @@ abstract contract ManagerBase is _getMessageSequenceStorage().num++; } + function _isSendTransceiverEnabledForChain( + address, // transceiver, + uint16 // chainId + ) internal view virtual returns (bool) { + return true; + } + + function _getEnabledRecvTransceiversForChain( + uint16 // forChainId + ) internal view virtual returns (uint64 bitmap) { + return type(uint64).max; + } + /// ============== Invariants ============================================= /// @dev When we add new immutables, this function should be updated @@ -541,8 +538,10 @@ abstract contract ManagerBase is function _checkThresholdInvariants() internal view { _checkThresholdInvariants(_getThresholdStorage().num); } - - function _checkThresholdInvariants(uint8 threshold) internal pure { + + function _checkThresholdInvariants( + uint8 threshold + ) internal pure { _NumTransceivers memory numTransceivers = _getNumTransceiversStorage(); // invariant: threshold <= enabledTransceivers.length diff --git a/evm/src/NttManager/NttManagerNoRateLimiting.sol b/evm/src/NttManager/NttManagerNoRateLimiting.sol index 82358c5e8..76c84f336 100644 --- a/evm/src/NttManager/NttManagerNoRateLimiting.sol +++ b/evm/src/NttManager/NttManagerNoRateLimiting.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache 2 pragma solidity >=0.8.8 <0.9.0; -import "./NttManager.sol"; +import "./NttManagerWithPerChainTransceivers.sol"; /// @title NttManagerNoRateLimiting /// @author Wormhole Project Contributors. @@ -10,12 +10,12 @@ import "./NttManager.sol"; /// free up code space. /// /// @dev All of the developer notes from `NttManager` apply here. -contract NttManagerNoRateLimiting is NttManager { +contract NttManagerNoRateLimiting is NttManagerWithPerChainTransceivers { constructor( address _token, Mode _mode, uint16 _chainId - ) NttManager(_token, _mode, _chainId, 0, true) {} + ) NttManagerWithPerChainTransceivers(_token, _mode, _chainId, 0, true) {} // ==================== Override RateLimiter functions ========================= diff --git a/evm/src/NttManager/NttManagerWithPerChainTransceivers.sol b/evm/src/NttManager/NttManagerWithPerChainTransceivers.sol new file mode 100644 index 000000000..9d66948c1 --- /dev/null +++ b/evm/src/NttManager/NttManagerWithPerChainTransceivers.sol @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity >=0.8.8 <0.9.0; + +import "./NttManager.sol"; + +/// @title NttManagerNoRateLimiting +/// @author Wormhole Project Contributors. +/// @notice The NttManagerNoRateLimiting abstract contract is an implementation of +/// NttManager that allows configuring different transceivers and thresholds +/// for each chain. Note that you can configure a different set of send and +/// receive transceivers for each chain, and if you don't specifically enable +/// any transceivers for a chain, then all transceivers will be used for it. +/// +/// @dev All of the developer notes from `NttManager` apply here. +abstract contract NttManagerWithPerChainTransceivers is NttManager { + constructor( + address _token, + Mode _mode, + uint16 _chainId, + uint64 _rateLimitDuration, + bool _skipRateLimiting + ) NttManager(_token, _mode, _chainId, _rateLimitDuration, _skipRateLimiting) {} + + bytes32 private constant SEND_TRANSCEIVER_BITMAP_SLOT = + bytes32(uint256(keccak256("nttpct.sendTransceiverBitmap")) - 1); + + bytes32 private constant RECV_TRANSCEIVER_BITMAP_SLOT = + bytes32(uint256(keccak256("nttpct.recvTransceiverBitmap")) - 1); + + // ==================== Override / implementation of transceiver stuff ========================= + + /// @inheritdoc IManagerBase + function enableSendTransceiverForChain( + address transceiver, + uint16 forChainId + ) external override(ManagerBase, IManagerBase) onlyOwner { + _enableTranceiverForChain(transceiver, forChainId, SEND_TRANSCEIVER_BITMAP_SLOT); + } + + /// @inheritdoc IManagerBase + function enableRecvTransceiverForChain( + address transceiver, + uint16 forChainId + ) external override(ManagerBase, IManagerBase) onlyOwner { + _enableTranceiverForChain(transceiver, forChainId, RECV_TRANSCEIVER_BITMAP_SLOT); + } + + function _enableTranceiverForChain( + address transceiver, + uint16 forChainId, + bytes32 tag + ) internal onlyOwner { + if (transceiver == address(0)) { + revert InvalidTransceiverZeroAddress(); + } + + mapping(address => TransceiverInfo) storage transceiverInfos = _getTransceiverInfosStorage(); + if (!transceiverInfos[transceiver].registered) { + revert NonRegisteredTransceiver(transceiver); + } + + uint8 index = _getTransceiverInfosStorage()[transceiver].index; + mapping(uint16 => _EnabledTransceiverBitmap) storage _bitmaps = + _getPerChainTransceiverBitmapStorage(tag); + _bitmaps[forChainId].bitmap |= uint64(1 << index); + + emit TransceiverEnabledForChain(transceiver, forChainId); + } + + function _isSendTransceiverEnabledForChain( + address transceiver, + uint16 forChainId + ) internal view override returns (bool) { + uint64 bitmap = + _getPerChainTransceiverBitmapStorage(SEND_TRANSCEIVER_BITMAP_SLOT)[forChainId].bitmap; + if (bitmap == 0) { + // NOTE: this makes it backwards compatible -- if the bitmap is not + // set, it's assumed the corridor uses all transceivers. + bitmap = type(uint64).max; + } + uint8 index = _getTransceiverInfosStorage()[transceiver].index; + return (bitmap & uint64(1 << index)) != 0; + } + + function _getEnabledRecvTransceiversForChain( + uint16 forChainId + ) internal view override returns (uint64 bitmap) { + bitmap = + _getPerChainTransceiverBitmapStorage(RECV_TRANSCEIVER_BITMAP_SLOT)[forChainId].bitmap; + if (bitmap == 0) { + // NOTE: this makes it backwards compatible -- if the bitmap is not + // set, it's assumed the corridor uses all transceivers. + bitmap = type(uint64).max; + } + } + + function _getPerChainTransceiverBitmapStorage( + bytes32 tag + ) internal pure returns (mapping(uint16 => _EnabledTransceiverBitmap) storage $) { + // TODO: this is safe (reusing the storage slot, because the mapping + // doesn't write into the slot itself) buy maybe we shouldn't? + uint256 slot = uint256(tag); + assembly ("memory-safe") { + $.slot := slot + } + } + + // ==================== Override / implementation of threshold stuff ========================= + + /// @inheritdoc IManagerBase + function setPerChainThreshold( + uint16 forChainId, + uint8 threshold + ) external override(ManagerBase, IManagerBase) onlyOwner { + if (threshold == 0) { + revert ZeroThreshold(); + } + + mapping(uint16 => _Threshold) storage _threshold = _getThresholdStoragePerChain(); + uint8 oldThreshold = _threshold[forChainId].num; + + _threshold[forChainId].num = threshold; + _checkThresholdInvariants(_threshold[forChainId].num); + + emit PerChainThresholdChanged(forChainId, oldThreshold, threshold); + } + + function getPerChainThreshold( + uint16 forChainId + ) public view override(ManagerBase, IManagerBase) returns (uint8) { + uint8 threshold = _getThresholdStoragePerChain()[forChainId].num; + if (threshold == 0) { + return _getThresholdStorage().num; + } + return threshold; + } + + function _getThresholdStoragePerChain() + private + pure + returns (mapping(uint16 => _Threshold) storage $) + { + // TODO: this is safe (reusing the storage slot, because the mapping + // doesn't write into the slot itself) buy maybe we shouldn't? + uint256 slot = uint256(THRESHOLD_SLOT); + assembly ("memory-safe") { + $.slot := slot + } + } +} diff --git a/evm/src/NttManager/TransceiverRegistry.sol b/evm/src/NttManager/TransceiverRegistry.sol index 7aa02f13e..d95f39b73 100644 --- a/evm/src/NttManager/TransceiverRegistry.sol +++ b/evm/src/NttManager/TransceiverRegistry.sol @@ -122,19 +122,6 @@ abstract contract TransceiverRegistry { } } - function _getPerChainTransceiverBitmapStorage() - private - pure - returns (mapping(uint16 => _EnabledTransceiverBitmap) storage $) - { - // TODO: this is safe (reusing the storage slot, because the mapping - // doesn't write into the slot itself) buy maybe we shouldn't? - uint256 slot = uint256(TRANSCEIVER_BITMAP_SLOT); - assembly ("memory-safe") { - $.slot := slot - } - } - function _getRegisteredTransceiversStorage() internal pure returns (address[] storage $) { uint256 slot = uint256(REGISTERED_TRANSCEIVERS_SLOT); assembly ("memory-safe") { @@ -151,33 +138,6 @@ abstract contract TransceiverRegistry { // =============== Storage Getters/Setters ======================================== - function _isTransceiverEnabledForChain( - address transceiver, - uint16 chainId - ) internal view returns (bool) { - uint64 bitmap = _getEnabledTransceiversBitmapForChain(chainId); - uint8 index = _getTransceiverInfosStorage()[transceiver].index; - return (bitmap & uint64(1 << index)) != 0; - } - - function _enableTransceiverForChain( - address transceiver, - uint16 chainId - ) internal { - if (transceiver == address(0)) { - revert InvalidTransceiverZeroAddress(); - } - - mapping(address => TransceiverInfo) storage transceiverInfos = _getTransceiverInfosStorage(); - if (!transceiverInfos[transceiver].registered) { - revert NonRegisteredTransceiver(transceiver); - } - - uint8 index = _getTransceiverInfosStorage()[transceiver].index; - mapping(uint16 => _EnabledTransceiverBitmap)storage _bitmaps = _getPerChainTransceiverBitmapStorage(); - _bitmaps[chainId].bitmap |= uint64(1 << index); - } - function _setTransceiver( address transceiver ) internal returns (uint8 index) { @@ -274,17 +234,6 @@ abstract contract TransceiverRegistry { return _getTransceiverBitmapStorage().bitmap; } - function _getEnabledTransceiversBitmapForChain( - uint16 forChainId - ) internal view virtual returns (uint64 bitmap) { - bitmap = _getPerChainTransceiverBitmapStorage()[forChainId].bitmap; - if (bitmap == 0) { - // NOTE: this makes it backwards compatible -- if the bitmap is not - // set, it's assumed the corridor uses all transceivers. - return type(uint64).max; - } - } - /// @notice Returns the Transceiver contracts that have been enabled via governance. function getTransceivers() external pure returns (address[] memory result) { result = _getEnabledTransceiversStorage(); diff --git a/evm/src/interfaces/IManagerBase.sol b/evm/src/interfaces/IManagerBase.sol index 8b255e77e..708b2380c 100644 --- a/evm/src/interfaces/IManagerBase.sol +++ b/evm/src/interfaces/IManagerBase.sol @@ -48,7 +48,7 @@ interface IManagerBase { /// @param oldThreshold The old threshold. /// @param threshold The new threshold. event ThresholdChanged(uint8 oldThreshold, uint8 threshold); - + /// @notice Emmitted when the per-chain threshold required transceivers is changed. /// @dev Topic0 /// 0x2a855b929b9a53c6fb5b5ed248b27e502b709c088e036a5aa17620c8fc5085a9. @@ -64,7 +64,7 @@ interface IManagerBase { /// @param transceiversNum The current number of transceivers. /// @param threshold The current threshold of transceivers. event TransceiverAdded(address transceiver, uint256 transceiversNum, uint8 threshold); - + /// @notice Emitted when a transceiver is enabled for a chain. /// @dev Topic0 /// 0xf05962b5774c658e85ed80c91a75af9d66d2af2253dda480f90bce78aff5eda5. @@ -127,6 +127,9 @@ interface IManagerBase { /// @param chainId The target Wormhole chain id error PeerNotRegistered(uint16 chainId); + /// @notice Feature is not implemented. + error NotImplemented(); + /// @notice Fetch the delivery price for a given recipient chain transfer. /// @param recipientChain The Wormhole chain ID of the transfer destination. /// @param transceiverInstructions The transceiver specific instructions for quoting and sending @@ -143,16 +146,13 @@ interface IManagerBase { function setThreshold( uint8 threshold ) external; - + /// @notice Sets the per-chain threshold for the number of attestations required for a message /// to be considered valid. Note that if a threshold is not specified for a chain, the default applies. /// @param chainId The chain for which the threshold applies. /// @param threshold The new threshold. /// @dev This method can only be executed by the `owner`. - function setThresholdPerChain( - uint16 chainId, - uint8 threshold - ) external; + function setPerChainThreshold(uint16 chainId, uint8 threshold) external; /// @notice Sets the transceiver for the given chain. /// @param transceiver The address of the transceiver. @@ -167,15 +167,18 @@ interface IManagerBase { function removeTransceiver( address transceiver ) external; - - /// @notice Enables the transceiver for the given chain. + + /// @notice Enables the transceiver for sending on the given chain. /// @param transceiver The address of the transceiver. /// @param chainId The chain for which the threshold applies. /// @dev This method can only be executed by the `owner`. - function enableTransceiverForChain( - address transceiver, - uint16 chainId - ) external; + function enableSendTransceiverForChain(address transceiver, uint16 chainId) external; + + /// @notice Enables the transceiver for receiving on the given chain. + /// @param transceiver The address of the transceiver. + /// @param chainId The chain for which the threshold applies. + /// @dev This method can only be executed by the `owner`. + function enableRecvTransceiverForChain(address transceiver, uint16 chainId) external; /// @notice Checks if a message has been approved. The message should have at least /// the minimum threshold of attestations from distinct endpoints. @@ -214,6 +217,13 @@ interface IManagerBase { /// it to be considered valid and acted upon. function getThreshold() external view returns (uint8); + /// @notice Returns the number of Transceivers that must attest to a msgId for + /// it to be considered valid and acted upon. + /// @param chainId The chain for which the threshold applies. + function getPerChainThreshold( + uint16 chainId + ) external view returns (uint8); + /// @notice Returns a boolean indicating if the transceiver has attested to the message. /// @param digest The digest of the message. /// @param index The index of the transceiver diff --git a/evm/src/interfaces/INttManager.sol b/evm/src/interfaces/INttManager.sol index 8e6ebf960..6497ff96d 100644 --- a/evm/src/interfaces/INttManager.sol +++ b/evm/src/interfaces/INttManager.sol @@ -141,9 +141,6 @@ interface INttManager is IManagerBase { /// @dev Selector 0x20371f2a. error InvalidPeerSameChainId(); - /// @notice Feature is not implemented. - error NotImplemented(); - /// @notice Transfer a given amount to a recipient on a given chain. This function is called /// by the user to send the token cross-chain. This function will either lock or burn the /// sender's tokens. Finally, this function will call into registered `Endpoint` contracts diff --git a/evm/test/IntegrationPerChainThresholds.t.sol b/evm/test/IntegrationPerChainThresholds.t.sol deleted file mode 100644 index 8f1ce529e..000000000 --- a/evm/test/IntegrationPerChainThresholds.t.sol +++ /dev/null @@ -1,736 +0,0 @@ -// SPDX-License-Identifier: Apache 2 -pragma solidity >=0.8.8 <0.9.0; - -import "forge-std/Test.sol"; -import "forge-std/console.sol"; - -import "../src/NttManager/NttManager.sol"; -import "../src/Transceiver/Transceiver.sol"; -import "../src/interfaces/INttManager.sol"; -import "../src/interfaces/IRateLimiter.sol"; -import "../src/interfaces/ITransceiver.sol"; -import "../src/interfaces/IManagerBase.sol"; -import "../src/interfaces/IRateLimiterEvents.sol"; -import {Utils} from "./libraries/Utils.sol"; -import {DummyToken, DummyTokenMintAndBurn} from "./NttManager.t.sol"; -import "../src/interfaces/IWormholeTransceiver.sol"; -import {WormholeTransceiver} from "../src/Transceiver/WormholeTransceiver/WormholeTransceiver.sol"; -import "../src/libraries/TransceiverStructs.sol"; -import "./mocks/MockNttManager.sol"; -import "./mocks/MockTransceivers.sol"; - -import "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; -import "openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -import "wormhole-solidity-sdk/interfaces/IWormhole.sol"; -import "wormhole-solidity-sdk/testing/helpers/WormholeSimulator.sol"; -import "wormhole-solidity-sdk/Utils.sol"; -//import "wormhole-solidity-sdk/testing/WormholeRelayerTest.sol"; - -contract TestPerChainTransceivers is Test, IRateLimiterEvents { - NttManager nttManagerChain1; - NttManager nttManagerChain2; - NttManager nttManagerChain3; - - using TrimmedAmountLib for uint256; - using TrimmedAmountLib for TrimmedAmount; - - uint16 constant chainId1 = 7; - uint16 constant chainId2 = 100; - uint16 constant chainId3 = 101; - uint8 constant FAST_CONSISTENCY_LEVEL = 200; - uint256 constant GAS_LIMIT = 500000; - - uint16 constant SENDING_CHAIN_ID = 1; - uint256 constant DEVNET_GUARDIAN_PK = - 0xcfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0; - WormholeSimulator guardian; - uint256 initialBlockTimestamp; - - WormholeTransceiver wormholeTransceiverChain1; - WormholeTransceiver secondWormholeTransceiverChain1; - WormholeTransceiver wormholeTransceiverChain2; - WormholeTransceiver secondWormholeTransceiverChain2; - WormholeTransceiver wormholeTransceiverChain3; - WormholeTransceiver secondWormholeTransceiverChain3; - address userA = address(0x123); - address userB = address(0x456); - address userC = address(0x789); - address userD = address(0xABC); - - address relayer = address(0x28D8F1Be96f97C1387e94A53e00eCcFb4E75175a); - IWormhole wormhole = IWormhole(0x4a8bc80Ed5a4067f1CCf107057b8270E0cC11A78); - - // This function sets up the following config: - // - A manager on each of three chains. - // - Two transceivers on each chain, all interconnected as peers. - // - On chain one, it sets a default threshold of one and a per-chain threshold of two for chain three. - // - On chain three, it sets a default threshold of one and a per-chain threshold of two for chain one. - - function setUp() public { - string memory url = "https://ethereum-sepolia-rpc.publicnode.com"; - vm.createSelectFork(url); - initialBlockTimestamp = vm.getBlockTimestamp(); - - guardian = new WormholeSimulator(address(wormhole), DEVNET_GUARDIAN_PK); - - vm.chainId(chainId1); - DummyToken t1 = new DummyToken(); - NttManager implementation = new MockNttManagerContract( - address(t1), IManagerBase.Mode.LOCKING, chainId1, 1 days, false - ); - - nttManagerChain1 = - MockNttManagerContract(address(new ERC1967Proxy(address(implementation), ""))); - nttManagerChain1.initialize(); - - // Create the first transceiver, from chain 1 to chain 2. - WormholeTransceiver wormholeTransceiverChain1Implementation = new MockWormholeTransceiverContract( - address(nttManagerChain1), - address(wormhole), - address(relayer), - address(0x0), - FAST_CONSISTENCY_LEVEL, - GAS_LIMIT - ); - wormholeTransceiverChain1 = MockWormholeTransceiverContract( - address(new ERC1967Proxy(address(wormholeTransceiverChain1Implementation), "")) - ); - - // Only the deployer should be able to initialize - vm.prank(userA); - vm.expectRevert( - abi.encodeWithSelector(ITransceiver.UnexpectedDeployer.selector, address(this), userA) - ); - wormholeTransceiverChain1.initialize(); - - // Actually initialize properly now - wormholeTransceiverChain1.initialize(); - - // Create the second transceiver for chain 1. - WormholeTransceiver secondWormholeTransceiverChain1Implementation = new MockWormholeTransceiverContract( - address(nttManagerChain1), - address(wormhole), - address(relayer), - address(0x0), - FAST_CONSISTENCY_LEVEL, - GAS_LIMIT - ); - secondWormholeTransceiverChain1 = MockWormholeTransceiverContract( - address(new ERC1967Proxy(address(secondWormholeTransceiverChain1Implementation), "")) - ); - - secondWormholeTransceiverChain1.initialize(); - - // Chain 2 setup - vm.chainId(chainId2); - DummyToken t2 = new DummyTokenMintAndBurn(); - NttManager implementationChain2 = new MockNttManagerContract( - address(t2), IManagerBase.Mode.BURNING, chainId2, 1 days, false - ); - - nttManagerChain2 = - MockNttManagerContract(address(new ERC1967Proxy(address(implementationChain2), ""))); - nttManagerChain2.initialize(); - - WormholeTransceiver wormholeTransceiverChain2Implementation = new MockWormholeTransceiverContract( - address(nttManagerChain2), - address(wormhole), - address(relayer), - address(0x0), - FAST_CONSISTENCY_LEVEL, - GAS_LIMIT - ); - wormholeTransceiverChain2 = MockWormholeTransceiverContract( - address(new ERC1967Proxy(address(wormholeTransceiverChain2Implementation), "")) - ); - wormholeTransceiverChain2.initialize(); - - // Register peer contracts for the nttManager and transceiver. Transceivers and nttManager each have the concept of peers here. - nttManagerChain1.setPeer( - chainId2, bytes32(uint256(uint160(address(nttManagerChain2)))), 9, type(uint64).max - ); - nttManagerChain2.setPeer( - chainId1, bytes32(uint256(uint160(address(nttManagerChain1)))), 7, type(uint64).max - ); - - // Create the second transceiver for chain 2. - WormholeTransceiver secondWormholeTransceiverChain2Implementation = new MockWormholeTransceiverContract( - address(nttManagerChain2), - address(wormhole), - address(relayer), - address(0x0), - FAST_CONSISTENCY_LEVEL, - GAS_LIMIT - ); - secondWormholeTransceiverChain2 = MockWormholeTransceiverContract( - address(new ERC1967Proxy(address(secondWormholeTransceiverChain2Implementation), "")) - ); - - secondWormholeTransceiverChain2.initialize(); - - // Set peers for the transceivers - wormholeTransceiverChain1.setWormholePeer( - chainId2, bytes32(uint256(uint160(address(wormholeTransceiverChain2)))) - ); - wormholeTransceiverChain2.setWormholePeer( - chainId1, bytes32(uint256(uint160(address(wormholeTransceiverChain1)))) - ); - secondWormholeTransceiverChain1.setWormholePeer( - chainId2, bytes32(uint256(uint160(address(secondWormholeTransceiverChain2)))) - ); - secondWormholeTransceiverChain2.setWormholePeer( - chainId1, bytes32(uint256(uint160(address(secondWormholeTransceiverChain1)))) - ); - - // Chain 3 setup - vm.chainId(chainId3); - DummyToken t3 = new DummyTokenMintAndBurn(); - NttManager implementationChain3 = new MockNttManagerContract( - address(t3), IManagerBase.Mode.BURNING, chainId3, 1 days, false - ); - - nttManagerChain3 = - MockNttManagerContract(address(new ERC1967Proxy(address(implementationChain3), ""))); - nttManagerChain3.initialize(); - - WormholeTransceiver wormholeTransceiverChain3Implementation = new MockWormholeTransceiverContract( - address(nttManagerChain3), - address(wormhole), - address(relayer), - address(0x0), - FAST_CONSISTENCY_LEVEL, - GAS_LIMIT - ); - wormholeTransceiverChain3 = MockWormholeTransceiverContract( - address(new ERC1967Proxy(address(wormholeTransceiverChain3Implementation), "")) - ); - wormholeTransceiverChain3.initialize(); - - // Register peer contracts for the nttManager and transceiver. Transceivers and nttManager each have the concept of peers here. - nttManagerChain1.setPeer( - chainId3, bytes32(uint256(uint160(address(nttManagerChain3)))), 9, type(uint64).max - ); - nttManagerChain3.setPeer( - chainId1, bytes32(uint256(uint160(address(nttManagerChain1)))), 7, type(uint64).max - ); - - // Create the second transceiver, from chain 3 to chain 1. - WormholeTransceiver secondWormholeTransceiverChain3Implementation = new MockWormholeTransceiverContract( - address(nttManagerChain3), - address(wormhole), - address(relayer), - address(0x0), - FAST_CONSISTENCY_LEVEL, - GAS_LIMIT - ); - secondWormholeTransceiverChain3 = MockWormholeTransceiverContract( - address(new ERC1967Proxy(address(secondWormholeTransceiverChain3Implementation), "")) - ); - - // Actually initialize properly now - secondWormholeTransceiverChain3.initialize(); - - // Set peers for the transceivers - wormholeTransceiverChain1.setWormholePeer( - chainId3, bytes32(uint256(uint160(address(wormholeTransceiverChain3)))) - ); - wormholeTransceiverChain3.setWormholePeer( - chainId1, bytes32(uint256(uint160(address(wormholeTransceiverChain1)))) - ); - wormholeTransceiverChain3.setWormholePeer( - chainId2, bytes32(uint256(uint160(address(wormholeTransceiverChain2)))) - ); - wormholeTransceiverChain2.setWormholePeer( - chainId3, bytes32(uint256(uint160(address(wormholeTransceiverChain3)))) - ); - secondWormholeTransceiverChain1.setWormholePeer( - chainId3, bytes32(uint256(uint160(address(secondWormholeTransceiverChain3)))) - ); - secondWormholeTransceiverChain3.setWormholePeer( - chainId1, bytes32(uint256(uint160(address(secondWormholeTransceiverChain1)))) - ); - secondWormholeTransceiverChain2.setWormholePeer( - chainId3, bytes32(uint256(uint160(address(secondWormholeTransceiverChain3)))) - ); - secondWormholeTransceiverChain3.setWormholePeer( - chainId2, bytes32(uint256(uint160(address(secondWormholeTransceiverChain2)))) - ); - } - - // This test does a transfer between chain one and chain two where there are two transceivers. - // Since chain two uses the default threshold of one, posting a VAA from only one transceiver completes the transfer. - function test_defaultThresholds() public { - nttManagerChain1.setTransceiver(address(wormholeTransceiverChain1)); - nttManagerChain1.setTransceiver(address(secondWormholeTransceiverChain1)); - nttManagerChain2.setTransceiver(address(wormholeTransceiverChain2)); - nttManagerChain2.setTransceiver(address(secondWormholeTransceiverChain2)); - nttManagerChain3.setTransceiver(address(wormholeTransceiverChain3)); - nttManagerChain3.setTransceiver(address(secondWormholeTransceiverChain3)); - - nttManagerChain1.setThreshold(1); - nttManagerChain2.setThreshold(1); - nttManagerChain3.setThreshold(1); - - // On chain 1, set the threshold to chain 3 to be different. - nttManagerChain1.setThresholdPerChain(chainId3, 2); - nttManagerChain3.setThresholdPerChain(chainId1, 2); - - require(nttManagerChain1.getThreshold() == 1, "Default threshold is wrong"); - require(nttManagerChain1.getThreshold(chainId3) == 2, "Threshold for chain 3 is wrong"); - - vm.chainId(chainId1); - - // Setting up the transfer - DummyToken token1 = DummyToken(nttManagerChain1.token()); - DummyToken token2 = DummyTokenMintAndBurn(nttManagerChain2.token()); - - uint8 decimals = token1.decimals(); - uint256 sendingAmount = 5 * 10 ** decimals; - token1.mintDummy(address(userA), 5 * 10 ** decimals); - - // Transfer tokens from chain one to chain two through standard means (not relayer) - vm.startPrank(userA); - token1.approve(address(nttManagerChain1), sendingAmount); - vm.recordLogs(); - { - uint256 nttManagerBalanceBefore = token1.balanceOf(address(nttManagerChain1)); - uint256 userBalanceBefore = token1.balanceOf(address(userA)); - nttManagerChain1.transfer(sendingAmount, chainId2, bytes32(uint256(uint160(userB)))); - - // Balance check on funds going in and out working as expected - uint256 nttManagerBalanceAfter = token1.balanceOf(address(nttManagerChain1)); - uint256 userBalanceAfter = token1.balanceOf(address(userB)); - require( - nttManagerBalanceBefore + sendingAmount == nttManagerBalanceAfter, - "Should be locking the tokens" - ); - require( - userBalanceBefore - sendingAmount == userBalanceAfter, - "User should have sent tokens" - ); - } - - vm.stopPrank(); - - // Get and sign the log to go down the other pipes. There should be two messages since we have two transceivers. - Vm.Log[] memory entries = guardian.fetchWormholeMessageFromLog(vm.getRecordedLogs()); - require(2 == entries.length, "Unexpected number of log entries 1"); - bytes[] memory encodedVMs = new bytes[](entries.length); - for (uint256 i = 0; i < encodedVMs.length; i++) { - encodedVMs[i] = guardian.fetchSignedMessageFromLogs(entries[i], chainId1); - } - - // Chain2 verification and checks - vm.chainId(chainId2); - - { - uint256 supplyBefore = token2.totalSupply(); - wormholeTransceiverChain2.receiveMessage(encodedVMs[0]); - uint256 supplyAfter = token2.totalSupply(); - - require(sendingAmount + supplyBefore == supplyAfter, "Supplies dont match"); - require(token2.balanceOf(userB) == sendingAmount, "User didn't receive tokens"); - require( - token2.balanceOf(address(nttManagerChain2)) == 0, "NttManager has unintended funds" - ); - } - - // Go back the other way from a THIRD user - vm.prank(userB); - token2.transfer(userC, sendingAmount); - - vm.startPrank(userC); - token2.approve(address(nttManagerChain2), sendingAmount); - vm.recordLogs(); - - // Supply checks on the transfer - { - uint256 supplyBefore = token2.totalSupply(); - nttManagerChain2.transfer( - sendingAmount, - chainId1, - toWormholeFormat(userD), - toWormholeFormat(userC), - false, - encodeTransceiverInstruction(true) - ); - - uint256 supplyAfter = token2.totalSupply(); - - require(sendingAmount - supplyBefore == supplyAfter, "Supplies don't match"); - require(token2.balanceOf(userB) == 0, "OG user receive tokens"); - require(token2.balanceOf(userC) == 0, "Sending user didn't receive tokens"); - require( - token2.balanceOf(address(nttManagerChain2)) == 0, - "NttManager didn't receive unintended funds" - ); - } - - // Get and sign the log to go down the other pipe. Thank you to whoever wrote this code in the past! - entries = guardian.fetchWormholeMessageFromLog(vm.getRecordedLogs()); - require(2 == entries.length, "Unexpected number of log entries 2"); - encodedVMs = new bytes[](entries.length); - for (uint256 i = 0; i < encodedVMs.length; i++) { - encodedVMs[i] = guardian.fetchSignedMessageFromLogs(entries[i], chainId2); - } - - vm.chainId(chainId1); - - // Receive the first message. Since the threshold is set to one, the transfer should complete. - { - uint256 supplyBefore = token1.totalSupply(); - wormholeTransceiverChain1.receiveMessage(encodedVMs[0]); - uint256 supplyAfter = token1.totalSupply(); - - require(supplyBefore == supplyAfter, "Supplies don't match between operations"); - require(token1.balanceOf(userB) == 0, "OG user receive tokens"); - require(token1.balanceOf(userC) == 0, "Sending user didn't receive tokens"); - require(token1.balanceOf(userD) == sendingAmount, "User received funds"); - } - - // Receive the second message. That should do nothing since we've already processed the transfer. - { - uint256 supplyBefore = token1.totalSupply(); - secondWormholeTransceiverChain1.receiveMessage(encodedVMs[1]); - uint256 supplyAfter = token1.totalSupply(); - - require(supplyBefore == supplyAfter, "Supplies don't match between operations"); - require(token1.balanceOf(userB) == 0, "OG user receive tokens"); - require(token1.balanceOf(userC) == 0, "Sending user didn't receive tokens"); - require(token1.balanceOf(userD) == sendingAmount, "User received funds"); - } - } - - // This test does a transfer between chain one and chain three where there are two transceivers and the per-chain threshold is two. - // The transfer is not completed until both VAAs are posted. - function test_perChainThreshold() public { - nttManagerChain1.setTransceiver(address(wormholeTransceiverChain1)); - nttManagerChain1.setTransceiver(address(secondWormholeTransceiverChain1)); - nttManagerChain2.setTransceiver(address(wormholeTransceiverChain2)); - nttManagerChain2.setTransceiver(address(secondWormholeTransceiverChain2)); - nttManagerChain3.setTransceiver(address(wormholeTransceiverChain3)); - nttManagerChain3.setTransceiver(address(secondWormholeTransceiverChain3)); - - nttManagerChain1.setThreshold(1); - nttManagerChain2.setThreshold(1); - nttManagerChain3.setThreshold(1); - - // On chain 1, set the threshold to chain 3 to be different. - nttManagerChain1.setThresholdPerChain(chainId3, 2); - nttManagerChain3.setThresholdPerChain(chainId1, 2); - - require(nttManagerChain1.getThreshold() == 1, "Default threshold is wrong"); - require(nttManagerChain1.getThreshold(chainId3) == 2, "Threshold for chain 3 is wrong"); - - vm.chainId(chainId1); - - // Setting up the transfer - DummyToken token1 = DummyToken(nttManagerChain1.token()); - DummyToken token3 = DummyTokenMintAndBurn(nttManagerChain3.token()); - - uint8 decimals = token1.decimals(); - uint256 sendingAmount = 5 * 10 ** decimals; - token1.mintDummy(address(userA), 5 * 10 ** decimals); - vm.startPrank(userA); - token1.approve(address(nttManagerChain1), sendingAmount); - - vm.recordLogs(); - - // Send token from chain 1 to chain 3, userB. - { - uint256 nttManagerBalanceBefore = token1.balanceOf(address(nttManagerChain1)); - uint256 userBalanceBefore = token1.balanceOf(address(userA)); - nttManagerChain1.transfer(sendingAmount, chainId3, bytes32(uint256(uint160(userB)))); - - // Balance check on funds going in and out working as expected - uint256 nttManagerBalanceAfter = token1.balanceOf(address(nttManagerChain1)); - uint256 userBalanceAfter = token1.balanceOf(address(userB)); - require( - nttManagerBalanceBefore + sendingAmount == nttManagerBalanceAfter, - "Should be locking the tokens" - ); - require( - userBalanceBefore - sendingAmount == userBalanceAfter, - "User should have sent tokens" - ); - } - - vm.stopPrank(); - - // Get and sign the log to go down the other pipes. There should be two messages since we have two transceivers. - Vm.Log[] memory entries = guardian.fetchWormholeMessageFromLog(vm.getRecordedLogs()); - require(2 == entries.length, "Unexpected number of log entries 3"); - bytes[] memory encodedVMs = new bytes[](entries.length); - for (uint256 i = 0; i < encodedVMs.length; i++) { - encodedVMs[i] = guardian.fetchSignedMessageFromLogs(entries[i], chainId1); - } - - // Chain3 verification and checks - vm.chainId(chainId3); - - uint256 supplyBefore = token3.totalSupply(); - - // Submit the first message on chain 3. The numbers shouldn't change yet since the threshold is two. - wormholeTransceiverChain3.receiveMessage(encodedVMs[0]); - uint256 supplyAfter = token3.totalSupply(); - - require(supplyBefore == supplyAfter, "Supplies changed early"); - require(token3.balanceOf(userB) == 0, "User receive tokens early"); - require(token3.balanceOf(address(nttManagerChain3)) == 0, "NttManager has unintended funds"); - - // Submit the second message and the transfer should complete. - secondWormholeTransceiverChain3.receiveMessage(encodedVMs[1]); - supplyAfter = token3.totalSupply(); - - require(sendingAmount + supplyBefore == supplyAfter, "Supplies dont match"); - require(token3.balanceOf(userB) == sendingAmount, "User didn't receive tokens"); - require(token3.balanceOf(address(nttManagerChain3)) == 0, "NttManager has unintended funds"); - - // Go back the other way from a THIRD user - vm.prank(userB); - token3.transfer(userC, sendingAmount); - - vm.startPrank(userC); - token3.approve(address(nttManagerChain3), sendingAmount); - vm.recordLogs(); - - // Supply checks on the transfer - supplyBefore = token3.totalSupply(); - nttManagerChain3.transfer( - sendingAmount, - chainId1, - toWormholeFormat(userD), - toWormholeFormat(userC), - false, - encodeTransceiverInstruction(true) - ); - - supplyAfter = token3.totalSupply(); - - require(sendingAmount - supplyBefore == supplyAfter, "Supplies don't match"); - require(token3.balanceOf(userB) == 0, "OG user receive tokens"); - require(token3.balanceOf(userC) == 0, "Sending user didn't receive tokens"); - require( - token3.balanceOf(address(nttManagerChain3)) == 0, - "NttManager didn't receive unintended funds" - ); - - // Get and sign the log to go down the other pipe. Thank you to whoever wrote this code in the past! - entries = guardian.fetchWormholeMessageFromLog(vm.getRecordedLogs()); - require(2 == entries.length, "Unexpected number of log entries for response"); - encodedVMs = new bytes[](entries.length); - for (uint256 i = 0; i < encodedVMs.length; i++) { - encodedVMs[i] = guardian.fetchSignedMessageFromLogs(entries[i], chainId3); - } - - // Chain1 verification and checks with the receiving of the message - vm.chainId(chainId1); - - // Submit the first message back on chain one. Nothing should happen because our threshold is two. - supplyBefore = token1.totalSupply(); - wormholeTransceiverChain1.receiveMessage(encodedVMs[0]); - supplyAfter = token1.totalSupply(); - - require(supplyBefore == supplyAfter, "Supplies don't match between operations"); - require(token1.balanceOf(userB) == 0, "OG user receive tokens"); - require(token1.balanceOf(userC) == 0, "Sending user didn't receive tokens"); - require(token1.balanceOf(userD) == 0, "User received funds before they should"); - - // Submit the second message back on chain one. This should update the balance. - supplyBefore = token1.totalSupply(); - secondWormholeTransceiverChain1.receiveMessage(encodedVMs[1]); - supplyAfter = token1.totalSupply(); - - require(supplyBefore == supplyAfter, "Supplies don't match between operations"); /////////////////// Is this right?? - require(token1.balanceOf(userB) == 0, "OG user receive tokens"); - require(token1.balanceOf(userC) == 0, "Sending user didn't receive tokens"); - require(token1.balanceOf(userD) == sendingAmount, "User received funds"); - } - - // This test does a transfer between chain one and chain three where there there is only one transceiver and the per-chain threshold is one. - // The transfer is not completed until both VAAs are posted. - function test_perChainTransceiver() public { - // By default, there are two transceivers and a threshold of two. - nttManagerChain1.setTransceiver(address(wormholeTransceiverChain1)); - nttManagerChain1.setTransceiver(address(secondWormholeTransceiverChain1)); - nttManagerChain2.setTransceiver(address(wormholeTransceiverChain2)); - nttManagerChain2.setTransceiver(address(secondWormholeTransceiverChain2)); - nttManagerChain3.setTransceiver(address(wormholeTransceiverChain3)); - nttManagerChain3.setTransceiver(address(secondWormholeTransceiverChain3)); - - nttManagerChain1.setThreshold(2); - nttManagerChain2.setThreshold(2); - nttManagerChain3.setThreshold(2); - - // Between chains one and three, there is only a single transceiver with a threshold of one. - nttManagerChain1.enableTransceiverForChain(address(wormholeTransceiverChain1), chainId3); - nttManagerChain3.enableTransceiverForChain(address(wormholeTransceiverChain3), chainId1); - nttManagerChain1.setThresholdPerChain(chainId3, 1); - nttManagerChain3.setThresholdPerChain(chainId1, 1); - - require(nttManagerChain1.getThreshold() == 2, "Default threshold is wrong on chain 1"); - require(nttManagerChain1.getThreshold(chainId3) == 1, "Threshold for chain 3 is wrong on chain 1"); - require(nttManagerChain1.getThreshold() == 2, "Default threshold is wrong on chain 3"); - require(nttManagerChain3.getThreshold(chainId1) == 1, "Threshold for chain 1 is wrong on chain 3"); - - vm.chainId(chainId1); - - // Setting up the transfer - DummyToken token1 = DummyToken(nttManagerChain1.token()); - DummyToken token3 = DummyTokenMintAndBurn(nttManagerChain3.token()); - - uint8 decimals = token1.decimals(); - uint256 sendingAmount = 5 * 10 ** decimals; - token1.mintDummy(address(userA), 5 * 10 ** decimals); - vm.startPrank(userA); - token1.approve(address(nttManagerChain1), sendingAmount); - - vm.recordLogs(); - - // Send token from chain 1 to chain 3, userB. - { - uint256 nttManagerBalanceBefore = token1.balanceOf(address(nttManagerChain1)); - uint256 userBalanceBefore = token1.balanceOf(address(userA)); - nttManagerChain1.transfer(sendingAmount, chainId3, bytes32(uint256(uint160(userB)))); - - // Balance check on funds going in and out working as expected - uint256 nttManagerBalanceAfter = token1.balanceOf(address(nttManagerChain1)); - uint256 userBalanceAfter = token1.balanceOf(address(userB)); - require( - nttManagerBalanceBefore + sendingAmount == nttManagerBalanceAfter, - "Should be locking the tokens" - ); - require( - userBalanceBefore - sendingAmount == userBalanceAfter, - "User should have sent tokens" - ); - } - - vm.stopPrank(); - - // Get and sign the log to go down the other pipes. There should be two messages since we have two transceivers. - Vm.Log[] memory entries = guardian.fetchWormholeMessageFromLog(vm.getRecordedLogs()); - require(2 == entries.length, "Unexpected number of log entries 3"); - bytes[] memory encodedVMs = new bytes[](entries.length); - for (uint256 i = 0; i < encodedVMs.length; i++) { - encodedVMs[i] = guardian.fetchSignedMessageFromLogs(entries[i], chainId1); - } - - // Chain3 verification and checks - vm.chainId(chainId3); - - uint256 supplyBefore = token3.totalSupply(); -/* - // Submit the first message on chain 3. The numbers shouldn't change yet since the threshold is two. - wormholeTransceiverChain3.receiveMessage(encodedVMs[0]); - uint256 supplyAfter = token3.totalSupply(); - - require(supplyBefore == supplyAfter, "Supplies changed early"); - require(token3.balanceOf(userB) == 0, "User receive tokens early"); - require(token3.balanceOf(address(nttManagerChain3)) == 0, "NttManager has unintended funds"); - - // Submit the second message and the transfer should complete. - secondWormholeTransceiverChain3.receiveMessage(encodedVMs[1]); - supplyAfter = token3.totalSupply(); - - require(sendingAmount + supplyBefore == supplyAfter, "Supplies dont match"); - require(token3.balanceOf(userB) == sendingAmount, "User didn't receive tokens"); - require(token3.balanceOf(address(nttManagerChain3)) == 0, "NttManager has unintended funds"); - - // Go back the other way from a THIRD user - vm.prank(userB); - token3.transfer(userC, sendingAmount); - - vm.startPrank(userC); - token3.approve(address(nttManagerChain3), sendingAmount); - vm.recordLogs(); - - // Supply checks on the transfer - supplyBefore = token3.totalSupply(); - nttManagerChain3.transfer( - sendingAmount, - chainId1, - toWormholeFormat(userD), - toWormholeFormat(userC), - false, - encodeTransceiverInstruction(true) - ); - - supplyAfter = token3.totalSupply(); - - require(sendingAmount - supplyBefore == supplyAfter, "Supplies don't match"); - require(token3.balanceOf(userB) == 0, "OG user receive tokens"); - require(token3.balanceOf(userC) == 0, "Sending user didn't receive tokens"); - require( - token3.balanceOf(address(nttManagerChain3)) == 0, - "NttManager didn't receive unintended funds" - ); - - // Get and sign the log to go down the other pipe. Thank you to whoever wrote this code in the past! - entries = guardian.fetchWormholeMessageFromLog(vm.getRecordedLogs()); - require(2 == entries.length, "Unexpected number of log entries for response"); - encodedVMs = new bytes[](entries.length); - for (uint256 i = 0; i < encodedVMs.length; i++) { - encodedVMs[i] = guardian.fetchSignedMessageFromLogs(entries[i], chainId3); - } - - // Chain1 verification and checks with the receiving of the message - vm.chainId(chainId1); - - // Submit the first message back on chain one. Nothing should happen because our threshold is two. - supplyBefore = token1.totalSupply(); - wormholeTransceiverChain1.receiveMessage(encodedVMs[0]); - supplyAfter = token1.totalSupply(); - - require(supplyBefore == supplyAfter, "Supplies don't match between operations"); - require(token1.balanceOf(userB) == 0, "OG user receive tokens"); - require(token1.balanceOf(userC) == 0, "Sending user didn't receive tokens"); - require(token1.balanceOf(userD) == 0, "User received funds before they should"); - - // Submit the second message back on chain one. This should update the balance. - supplyBefore = token1.totalSupply(); - secondWormholeTransceiverChain1.receiveMessage(encodedVMs[1]); - supplyAfter = token1.totalSupply(); - - require(supplyBefore == supplyAfter, "Supplies don't match between operations"); /////////////////// Is this right?? - require(token1.balanceOf(userB) == 0, "OG user receive tokens"); - require(token1.balanceOf(userC) == 0, "Sending user didn't receive tokens"); - require(token1.balanceOf(userD) == sendingAmount, "User received funds"); - */ - } - - function encodeTransceiverInstruction(bool relayer_off) public view returns (bytes memory) { - WormholeTransceiver.WormholeTransceiverInstruction memory instruction = - IWormholeTransceiver.WormholeTransceiverInstruction(relayer_off); - bytes memory encodedInstructionWormhole = - wormholeTransceiverChain1.encodeWormholeTransceiverInstruction(instruction); - TransceiverStructs.TransceiverInstruction memory TransceiverInstruction = TransceiverStructs - .TransceiverInstruction({index: 0, payload: encodedInstructionWormhole}); - TransceiverStructs.TransceiverInstruction[] memory TransceiverInstructions = - new TransceiverStructs.TransceiverInstruction[](1); - TransceiverInstructions[0] = TransceiverInstruction; - return TransceiverStructs.encodeTransceiverInstructions(TransceiverInstructions); - } - - // Encode an instruction for each of the relayers - function encodeTransceiverInstructions(bool relayer_off) public view returns (bytes memory) { - WormholeTransceiver.WormholeTransceiverInstruction memory instruction = - IWormholeTransceiver.WormholeTransceiverInstruction(relayer_off); - - bytes memory encodedInstructionWormhole = - wormholeTransceiverChain1.encodeWormholeTransceiverInstruction(instruction); - - TransceiverStructs.TransceiverInstruction memory TransceiverInstruction1 = - TransceiverStructs.TransceiverInstruction({index: 0, payload: encodedInstructionWormhole}); - TransceiverStructs.TransceiverInstruction memory TransceiverInstruction2 = - TransceiverStructs.TransceiverInstruction({index: 1, payload: encodedInstructionWormhole}); - - TransceiverStructs.TransceiverInstruction[] memory TransceiverInstructions = - new TransceiverStructs.TransceiverInstruction[](2); - - TransceiverInstructions[0] = TransceiverInstruction1; - TransceiverInstructions[1] = TransceiverInstruction2; - - return TransceiverStructs.encodeTransceiverInstructions(TransceiverInstructions); - } -} diff --git a/evm/test/IntegrationWithoutRateLimiting.t.sol b/evm/test/IntegrationWithoutRateLimiting.t.sol index 8d7e5d0fe..83951297b 100755 --- a/evm/test/IntegrationWithoutRateLimiting.t.sol +++ b/evm/test/IntegrationWithoutRateLimiting.t.sol @@ -313,19 +313,19 @@ contract TestEndToEndNoRateLimiting is Test { chainId1, bytes32(uint256(uint160(address(nttManagerChain2)))), 9, type(uint64).max ); - vm.expectRevert(abi.encodeWithSelector(INttManager.NotImplemented.selector)); + vm.expectRevert(abi.encodeWithSelector(IManagerBase.NotImplemented.selector)); nttManagerChain1.getOutboundQueuedTransfer(0); - vm.expectRevert(abi.encodeWithSelector(INttManager.NotImplemented.selector)); + vm.expectRevert(abi.encodeWithSelector(IManagerBase.NotImplemented.selector)); nttManagerChain1.getInboundQueuedTransfer(bytes32(0)); - vm.expectRevert(abi.encodeWithSelector(INttManager.NotImplemented.selector)); + vm.expectRevert(abi.encodeWithSelector(IManagerBase.NotImplemented.selector)); nttManagerChain1.completeInboundQueuedTransfer(bytes32(0)); - vm.expectRevert(abi.encodeWithSelector(INttManager.NotImplemented.selector)); + vm.expectRevert(abi.encodeWithSelector(IManagerBase.NotImplemented.selector)); nttManagerChain1.completeOutboundQueuedTransfer(0); - vm.expectRevert(abi.encodeWithSelector(INttManager.NotImplemented.selector)); + vm.expectRevert(abi.encodeWithSelector(IManagerBase.NotImplemented.selector)); nttManagerChain1.cancelOutboundQueuedTransfer(0); } @@ -572,6 +572,7 @@ contract TestEndToEndNoRateLimiting is Test { uint256 supplyBefore = token2.totalSupply(); wormholeTransceiverChain2_1.receiveMessage(encodedVMs[0]); + // Playing the VAA to the wrong transceiver should revert. vm.expectRevert( abi.encodeWithSelector( IWormholeTransceiver.InvalidWormholePeer.selector, diff --git a/evm/test/NttManagerNoRateLimiting.t.sol b/evm/test/NttManagerNoRateLimiting.t.sol index 9df4d4d80..339429fce 100644 --- a/evm/test/NttManagerNoRateLimiting.t.sol +++ b/evm/test/NttManagerNoRateLimiting.t.sol @@ -750,11 +750,11 @@ contract TestNttManagerNoRateLimiting is Test, IRateLimiterEvents { vm.chainId(chainId); // Queued outbound transfers can't be completed, per usual - vm.expectRevert(abi.encodeWithSelector(INttManager.NotImplemented.selector)); + vm.expectRevert(abi.encodeWithSelector(IManagerBase.NotImplemented.selector)); nttManager.completeOutboundQueuedTransfer(sequence); // Queued outbound transfers can't be cancelled, per usual - vm.expectRevert(abi.encodeWithSelector(INttManager.NotImplemented.selector)); + vm.expectRevert(abi.encodeWithSelector(IManagerBase.NotImplemented.selector)); nttManager.cancelOutboundQueuedTransfer(sequence); // Outbound transfers fail when queued @@ -823,7 +823,7 @@ contract TestNttManagerNoRateLimiting is Test, IRateLimiterEvents { vm.warp(vm.getBlockTimestamp() + 1 days); - vm.expectRevert(abi.encodeWithSelector(INttManager.NotImplemented.selector)); + vm.expectRevert(abi.encodeWithSelector(IManagerBase.NotImplemented.selector)); nttManager.completeInboundQueuedTransfer(hash); } diff --git a/evm/test/IntegrationPerChainTransceivers.t.sol b/evm/test/PerChainTransceivers.t.sol similarity index 82% rename from evm/test/IntegrationPerChainTransceivers.t.sol rename to evm/test/PerChainTransceivers.t.sol index ab8ff8fe7..1514ef445 100755 --- a/evm/test/IntegrationPerChainTransceivers.t.sol +++ b/evm/test/PerChainTransceivers.t.sol @@ -4,13 +4,14 @@ pragma solidity >=0.8.8 <0.9.0; import "forge-std/Test.sol"; import "forge-std/console.sol"; -import "../src/NttManager/NttManager.sol"; +import "../src/NttManager/NttManagerNoRateLimiting.sol"; import "../src/Transceiver/Transceiver.sol"; import "../src/interfaces/INttManager.sol"; import "../src/interfaces/IRateLimiter.sol"; import "../src/interfaces/ITransceiver.sol"; import "../src/interfaces/IManagerBase.sol"; import "../src/interfaces/IRateLimiterEvents.sol"; +import "../src/NttManager/TransceiverRegistry.sol"; import {Utils} from "./libraries/Utils.sol"; import {DummyToken, DummyTokenMintAndBurn} from "./NttManager.t.sol"; import "../src/interfaces/IWormholeTransceiver.sol"; @@ -26,10 +27,10 @@ import "wormhole-solidity-sdk/testing/helpers/WormholeSimulator.sol"; import "wormhole-solidity-sdk/Utils.sol"; //import "wormhole-solidity-sdk/testing/WormholeRelayerTest.sol"; -contract TestPerChainThresholds is Test, IRateLimiterEvents { - NttManager nttManagerChain1; - NttManager nttManagerChain2; - NttManager nttManagerChain3; +contract TestPerChainTransceivers is Test, IRateLimiterEvents { + MockNttManagerNoRateLimitingContractForTest nttManagerChain1; + NttManagerNoRateLimiting nttManagerChain2; + NttManagerNoRateLimiting nttManagerChain3; using TrimmedAmountLib for uint256; using TrimmedAmountLib for TrimmedAmount; @@ -75,12 +76,13 @@ contract TestPerChainThresholds is Test, IRateLimiterEvents { vm.chainId(chainId1); DummyToken t1 = new DummyToken(); - NttManager implementation = new MockNttManagerContract( - address(t1), IManagerBase.Mode.LOCKING, chainId1, 1 days, false + NttManager implementation = new MockNttManagerNoRateLimitingContractForTest( + address(t1), IManagerBase.Mode.LOCKING, chainId1 ); - nttManagerChain1 = - MockNttManagerContract(address(new ERC1967Proxy(address(implementation), ""))); + nttManagerChain1 = MockNttManagerNoRateLimitingContractForTest( + address(new ERC1967Proxy(address(implementation), "")) + ); nttManagerChain1.initialize(); // Create the first transceiver, from chain 1 to chain 2. @@ -107,9 +109,6 @@ contract TestPerChainThresholds is Test, IRateLimiterEvents { wormholeTransceiverChain1.initialize(); nttManagerChain1.setTransceiver(address(wormholeTransceiverChain1)); - nttManagerChain1.setOutboundLimit(type(uint64).max); - nttManagerChain1.setInboundLimit(type(uint64).max, chainId2); - nttManagerChain1.setInboundLimit(type(uint64).max, chainId3); // Create the second transceiver for chain 1. WormholeTransceiver secondWormholeTransceiverChain1Implementation = new MockWormholeTransceiverContract( @@ -130,12 +129,13 @@ contract TestPerChainThresholds is Test, IRateLimiterEvents { // Chain 2 setup vm.chainId(chainId2); DummyToken t2 = new DummyTokenMintAndBurn(); - NttManager implementationChain2 = new MockNttManagerContract( - address(t2), IManagerBase.Mode.BURNING, chainId2, 1 days, false + NttManager implementationChain2 = new MockNttManagerNoRateLimitingContract( + address(t2), IManagerBase.Mode.BURNING, chainId2 ); - nttManagerChain2 = - MockNttManagerContract(address(new ERC1967Proxy(address(implementationChain2), ""))); + nttManagerChain2 = MockNttManagerNoRateLimitingContract( + address(new ERC1967Proxy(address(implementationChain2), "")) + ); nttManagerChain2.initialize(); WormholeTransceiver wormholeTransceiverChain2Implementation = new MockWormholeTransceiverContract( @@ -152,8 +152,6 @@ contract TestPerChainThresholds is Test, IRateLimiterEvents { wormholeTransceiverChain2.initialize(); nttManagerChain2.setTransceiver(address(wormholeTransceiverChain2)); - nttManagerChain2.setOutboundLimit(type(uint64).max); - nttManagerChain2.setInboundLimit(type(uint64).max, chainId1); // Register peer contracts for the nttManager and transceiver. Transceivers and nttManager each have the concept of peers here. nttManagerChain1.setPeer( @@ -196,12 +194,13 @@ contract TestPerChainThresholds is Test, IRateLimiterEvents { // Chain 3 setup vm.chainId(chainId3); DummyToken t3 = new DummyTokenMintAndBurn(); - NttManager implementationChain3 = new MockNttManagerContract( - address(t3), IManagerBase.Mode.BURNING, chainId3, 1 days, false + NttManager implementationChain3 = new MockNttManagerNoRateLimitingContract( + address(t3), IManagerBase.Mode.BURNING, chainId3 ); - nttManagerChain3 = - MockNttManagerContract(address(new ERC1967Proxy(address(implementationChain3), ""))); + nttManagerChain3 = MockNttManagerNoRateLimitingContract( + address(new ERC1967Proxy(address(implementationChain3), "")) + ); nttManagerChain3.initialize(); WormholeTransceiver wormholeTransceiverChain3Implementation = new MockWormholeTransceiverContract( @@ -218,8 +217,6 @@ contract TestPerChainThresholds is Test, IRateLimiterEvents { wormholeTransceiverChain3.initialize(); nttManagerChain3.setTransceiver(address(wormholeTransceiverChain3)); - nttManagerChain3.setOutboundLimit(type(uint64).max); - nttManagerChain3.setInboundLimit(type(uint64).max, chainId1); // Register peer contracts for the nttManager and transceiver. Transceivers and nttManager each have the concept of peers here. nttManagerChain1.setPeer( @@ -246,8 +243,6 @@ contract TestPerChainThresholds is Test, IRateLimiterEvents { secondWormholeTransceiverChain3.initialize(); nttManagerChain3.setTransceiver(address(secondWormholeTransceiverChain3)); - nttManagerChain1.setOutboundLimit(type(uint64).max); - nttManagerChain1.setInboundLimit(type(uint64).max, chainId3); // Set peers for the transceivers wormholeTransceiverChain1.setWormholePeer( @@ -284,11 +279,85 @@ contract TestPerChainThresholds is Test, IRateLimiterEvents { nttManagerChain3.setThreshold(1); // On chain 1, set the threshold to chain 3 to be different. - nttManagerChain1.setThresholdPerChain(chainId3, 2); - nttManagerChain3.setThresholdPerChain(chainId1, 2); + nttManagerChain1.setPerChainThreshold(chainId3, 2); + nttManagerChain3.setPerChainThreshold(chainId1, 2); require(nttManagerChain1.getThreshold() == 1, "Default threshold is wrong"); - require(nttManagerChain1.getThreshold(chainId3) == 2, "Threshold for chain 3 is wrong"); + require( + nttManagerChain1.getPerChainThreshold(chainId3) == 2, "Threshold for chain 3 is wrong" + ); + + // Since we haven't set the per-chain threshold for chain 2, it should return the default + require( + nttManagerChain1.getPerChainThreshold(chainId2) == 1, "Threshold for chain 2 is wrong" + ); + } + + function test_transceiverSetters() public { + // If we haven't enabled per-chain send transceivers for this chain, the getter should return true. + require( + nttManagerChain1.isSendTransceiverEnabledForChain(address(nttManagerChain3), chainId3), + "Send transceiver should be enabled by default" + ); + + // If we haven't enabled per-chain receive transceivers, the getter should return everything is enabled. + require( + nttManagerChain1.getEnabledRecvTransceiversForChain(chainId3) == type(uint64).max, + "Receive transceiver should be enabled by default" + ); + + nttManagerChain1.enableSendTransceiverForChain(address(wormholeTransceiverChain1), chainId2); + nttManagerChain1.enableRecvTransceiverForChain( + address(secondWormholeTransceiverChain1), chainId3 + ); + + // Once we enable a transceiver for a chain, that is the only one enabled for that chain. + require( + nttManagerChain1.isSendTransceiverEnabledForChain( + address(wormholeTransceiverChain1), chainId2 + ), + "First transceiver should be enabled for sending on chain 2" + ); + require( + !nttManagerChain1.isSendTransceiverEnabledForChain( + address(secondWormholeTransceiverChain1), chainId2 + ), + "Second transceiver should not be enabled for sending on chain 2" + ); + require( + nttManagerChain1.getEnabledRecvTransceiversForChain(chainId3) == 0x2, + "Only second transceiver should be enabled for receiving on chain 3" + ); + + // And for chains we didn't touch, the defaults should still be in place. + require( + nttManagerChain1.isSendTransceiverEnabledForChain(address(nttManagerChain3), chainId3), + "Send transceiver should be enabled by default for untouched chain" + ); + require( + nttManagerChain1.getEnabledRecvTransceiversForChain(chainId2) == type(uint64).max, + "Receive transceiver should be enabled by default for untouched chain" + ); + } + + function test_someReverts() public { + // Can't enable transceiver with address zero. + vm.expectRevert( + abi.encodeWithSelector(TransceiverRegistry.InvalidTransceiverZeroAddress.selector) + ); + nttManagerChain1.enableSendTransceiverForChain(address(0), chainId2); + + // Can't enable transceiver for an unknown address. + vm.expectRevert( + abi.encodeWithSelector( + TransceiverRegistry.NonRegisteredTransceiver.selector, address(this) + ) + ); + nttManagerChain1.enableSendTransceiverForChain(address(this), chainId2); + + // Can set the per-chain threshold to zero. + vm.expectRevert(abi.encodeWithSelector(IManagerBase.ZeroThreshold.selector)); + nttManagerChain1.setPerChainThreshold(chainId2, 0); } // This test does a transfer between chain one and chain two. @@ -390,9 +459,9 @@ contract TestPerChainThresholds is Test, IRateLimiterEvents { encodedVMs[i] = guardian.fetchSignedMessageFromLogs(entries[i], chainId2); } + // Chain1 verification and checks with the receiving of the message vm.chainId(chainId1); - // Receive the first message. Since the threshold is set to one, the transfer should complete. { uint256 supplyBefore = token1.totalSupply(); wormholeTransceiverChain1.receiveMessage(encodedVMs[0]); @@ -404,17 +473,7 @@ contract TestPerChainThresholds is Test, IRateLimiterEvents { require(token1.balanceOf(userD) == sendingAmount, "User received funds"); } - // Receive the second message. That should do nothing since we've already processed the transfer. - { - uint256 supplyBefore = token1.totalSupply(); - secondWormholeTransceiverChain1.receiveMessage(encodedVMs[1]); - uint256 supplyAfter = token1.totalSupply(); - - require(supplyBefore == supplyAfter, "Supplies don't match between operations"); - require(token1.balanceOf(userB) == 0, "OG user receive tokens"); - require(token1.balanceOf(userC) == 0, "Sending user didn't receive tokens"); - require(token1.balanceOf(userD) == sendingAmount, "User received funds"); - } + //////////////////////// receive message 1. } // This test does a transfer between chain one and chain three. @@ -545,7 +604,9 @@ contract TestPerChainThresholds is Test, IRateLimiterEvents { require(token1.balanceOf(userD) == sendingAmount, "User received funds"); } - function encodeTransceiverInstruction(bool relayer_off) public view returns (bytes memory) { + function encodeTransceiverInstruction( + bool relayer_off + ) public view returns (bytes memory) { WormholeTransceiver.WormholeTransceiverInstruction memory instruction = IWormholeTransceiver.WormholeTransceiverInstruction(relayer_off); bytes memory encodedInstructionWormhole = @@ -559,7 +620,9 @@ contract TestPerChainThresholds is Test, IRateLimiterEvents { } // Encode an instruction for each of the relayers - function encodeTransceiverInstructions(bool relayer_off) public view returns (bytes memory) { + function encodeTransceiverInstructions( + bool relayer_off + ) public view returns (bytes memory) { WormholeTransceiver.WormholeTransceiverInstruction memory instruction = IWormholeTransceiver.WormholeTransceiverInstruction(relayer_off); diff --git a/evm/test/PerChainTransceiversDemo.t.sol b/evm/test/PerChainTransceiversDemo.t.sol new file mode 100755 index 000000000..eb7f575b5 --- /dev/null +++ b/evm/test/PerChainTransceiversDemo.t.sol @@ -0,0 +1,430 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity >=0.8.8 <0.9.0; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +import "../src/NttManager/NttManagerNoRateLimiting.sol"; +import "../src/Transceiver/Transceiver.sol"; +import "../src/interfaces/INttManager.sol"; +import "../src/interfaces/IRateLimiter.sol"; +import "../src/interfaces/ITransceiver.sol"; +import "../src/interfaces/IManagerBase.sol"; +import "../src/interfaces/IRateLimiterEvents.sol"; +import {Utils} from "./libraries/Utils.sol"; +import {DummyToken, DummyTokenMintAndBurn} from "./NttManager.t.sol"; +import "../src/interfaces/IWormholeTransceiver.sol"; +import {WormholeTransceiver} from "../src/Transceiver/WormholeTransceiver/WormholeTransceiver.sol"; +import "../src/libraries/TransceiverStructs.sol"; +import "./mocks/MockNttManager.sol"; +import "./mocks/MockTransceivers.sol"; + +import "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; +import "openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import "wormhole-solidity-sdk/interfaces/IWormhole.sol"; +import "wormhole-solidity-sdk/testing/helpers/WormholeSimulator.sol"; +import "wormhole-solidity-sdk/Utils.sol"; +//import "wormhole-solidity-sdk/testing/WormholeRelayerTest.sol"; + +// This contract demonstrates how per-chain transceivers could be used based on the following configuration. +// +// Chain1 ↔ Chain2 +// +// 1. Chain1 → Chain2 +// a. Send (on Chain1): via both transceivers +// b. Receive (on Chain2): Require 2-of-2, both transceivers +// 2. Chain2 → Chain1 +// a. Send (on Chain2): via first transceiver only +// b. Receive (on Chain1): require first transceiver only (1-of-?) +// +// For this test, we will use the mock wormhole transceiver for both, but will configure separate ones +// with the appropriate thresholds. We will then do a transfer. + +contract TestPerChainTransceiversDemo is Test, IRateLimiterEvents { + MockNttManagerNoRateLimitingContractForTest nttManagerChain1; + MockNttManagerNoRateLimitingContractForTest nttManagerChain2; + + using TrimmedAmountLib for uint256; + using TrimmedAmountLib for TrimmedAmount; + + uint16 constant chainId1 = 101; + uint16 constant chainId2 = 102; + uint8 constant FAST_CONSISTENCY_LEVEL = 200; + uint256 constant GAS_LIMIT = 500000; + + uint16 constant SENDING_CHAIN_ID = 2; + uint256 constant DEVNET_GUARDIAN_PK = + 0xcfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0; + WormholeSimulator guardian; + uint256 initialBlockTimestamp; + + WormholeTransceiver chainOneFirstTransceiver; + WormholeTransceiver chainOneSecondTransceiver; + WormholeTransceiver chainTwoFirstTransceiver; + WormholeTransceiver chainTwoSecondTransceiver; + + address userA = address(0x123); + address userB = address(0x456); + address userC = address(0x789); + address userD = address(0xABC); + + address relayer = address(0x28D8F1Be96f97C1387e94A53e00eCcFb4E75175a); + IWormhole wormhole = IWormhole(0x4a8bc80Ed5a4067f1CCf107057b8270E0cC11A78); + + function setUp() public { + string memory url = "https://ethereum-sepolia-rpc.publicnode.com"; + vm.createSelectFork(url); + initialBlockTimestamp = vm.getBlockTimestamp(); + + guardian = new WormholeSimulator(address(wormhole), DEVNET_GUARDIAN_PK); + + // Set up the manager on chain one. ////////////////////////////////////////////////////////////////// + vm.chainId(chainId1); + DummyToken t1 = new DummyToken(); + NttManager implementation = new MockNttManagerNoRateLimitingContractForTest( + address(t1), IManagerBase.Mode.LOCKING, chainId1 + ); + + nttManagerChain1 = MockNttManagerNoRateLimitingContractForTest( + address(new ERC1967Proxy(address(implementation), "")) + ); + nttManagerChain1.initialize(); + + // Create the first transceiver on chain one. + WormholeTransceiver chainOneFirstTransceiverImplementation = new MockWormholeTransceiverContract( + address(nttManagerChain1), + address(wormhole), + address(relayer), + address(0x0), + FAST_CONSISTENCY_LEVEL, + GAS_LIMIT + ); + chainOneFirstTransceiver = MockWormholeTransceiverContract( + address(new ERC1967Proxy(address(chainOneFirstTransceiverImplementation), "")) + ); + + chainOneFirstTransceiver.initialize(); + nttManagerChain1.setTransceiver(address(chainOneFirstTransceiver)); + + // Create the second transceiver for chain one. + WormholeTransceiver chainOneSecondTransceiverImplementation = new MockWormholeTransceiverContract( + address(nttManagerChain1), + address(wormhole), + address(relayer), + address(0x0), + FAST_CONSISTENCY_LEVEL, + GAS_LIMIT + ); + chainOneSecondTransceiver = MockWormholeTransceiverContract( + address(new ERC1967Proxy(address(chainOneSecondTransceiverImplementation), "")) + ); + + chainOneSecondTransceiver.initialize(); + nttManagerChain1.setTransceiver(address(chainOneSecondTransceiver)); + + // Set up the manager on chain two. ////////////////////////////////////////////////////////////////// + vm.chainId(chainId2); + DummyToken t2 = new DummyTokenMintAndBurn(); + NttManager implementationChain2 = new MockNttManagerNoRateLimitingContractForTest( + address(t2), IManagerBase.Mode.BURNING, chainId2 + ); + + nttManagerChain2 = MockNttManagerNoRateLimitingContractForTest( + address(new ERC1967Proxy(address(implementationChain2), "")) + ); + nttManagerChain2.initialize(); + + // Create the first transceiver on chain two. + WormholeTransceiver chainTwoFirstTransceiverImplementation = new MockWormholeTransceiverContract( + address(nttManagerChain2), + address(wormhole), + address(relayer), + address(0x0), + FAST_CONSISTENCY_LEVEL, + GAS_LIMIT + ); + chainTwoFirstTransceiver = MockWormholeTransceiverContract( + address(new ERC1967Proxy(address(chainTwoFirstTransceiverImplementation), "")) + ); + chainTwoFirstTransceiver.initialize(); + + nttManagerChain2.setTransceiver(address(chainTwoFirstTransceiver)); + + // Create the second transceiver on chain two. + WormholeTransceiver chainTwoSecondTransceiverImplementation = new MockWormholeTransceiverContract( + address(nttManagerChain2), + address(wormhole), + address(relayer), + address(0x0), + FAST_CONSISTENCY_LEVEL, + GAS_LIMIT + ); + chainTwoSecondTransceiver = MockWormholeTransceiverContract( + address(new ERC1967Proxy(address(chainTwoSecondTransceiverImplementation), "")) + ); + + chainTwoSecondTransceiver.initialize(); + nttManagerChain2.setTransceiver(address(chainTwoSecondTransceiver)); + + // Register the two NTT manager peers. ////////////////////////////////////////////////////////////////// + nttManagerChain1.setPeer( + chainId2, bytes32(uint256(uint160(address(nttManagerChain2)))), 9, type(uint64).max + ); + nttManagerChain2.setPeer( + chainId1, bytes32(uint256(uint160(address(nttManagerChain1)))), 7, type(uint64).max + ); + + // Register the transceiver peers. ////////////////////////////////////////////////////////////////// + chainOneFirstTransceiver.setWormholePeer( + chainId2, bytes32(uint256(uint160(address(chainTwoFirstTransceiver)))) + ); + chainTwoFirstTransceiver.setWormholePeer( + chainId1, bytes32(uint256(uint160(address(chainOneFirstTransceiver)))) + ); + chainOneSecondTransceiver.setWormholePeer( + chainId2, bytes32(uint256(uint160(address(chainTwoSecondTransceiver)))) + ); + chainTwoSecondTransceiver.setWormholePeer( + chainId1, bytes32(uint256(uint160(address(chainOneSecondTransceiver)))) + ); + + // Set the default thresholds. ////////////////////////////////////////////////////////////////////// + nttManagerChain1.setThreshold(2); + nttManagerChain2.setThreshold(2); + + // Set up our per-chain transceivers and thresholds. //////////////////////////////////////////////// + + // Set up chain one. + // 1.a: + nttManagerChain1.enableSendTransceiverForChain(address(chainOneFirstTransceiver), chainId2); + nttManagerChain1.enableSendTransceiverForChain(address(chainOneSecondTransceiver), chainId2); + // 2.b: + nttManagerChain1.enableRecvTransceiverForChain(address(chainOneFirstTransceiver), chainId2); + nttManagerChain1.setPerChainThreshold(chainId2, 1); + + // Set up chain two. + // 2.a: + nttManagerChain2.enableSendTransceiverForChain(address(chainTwoFirstTransceiver), chainId1); + // 1.b: + nttManagerChain2.enableRecvTransceiverForChain(address(chainTwoFirstTransceiver), chainId1); + nttManagerChain2.enableRecvTransceiverForChain(address(chainTwoSecondTransceiver), chainId1); + nttManagerChain2.setPerChainThreshold(chainId1, 2); + } + + function test_verifyConfig() public view { + // Verify config of chain one. ////////////////////////////////////////////////////////// + require( + nttManagerChain1.isSendTransceiverEnabledForChain( + address(chainOneFirstTransceiver), chainId2 + ), + "On chain 1, first transceiver should be enabled for sending to chain 2" + ); + require( + nttManagerChain1.isSendTransceiverEnabledForChain( + address(chainOneFirstTransceiver), chainId2 + ), + "On chain 1, second transceiver should be enabled for sending to chain 2" + ); + require( + nttManagerChain1.getEnabledRecvTransceiversForChain(chainId2) == 0x1, + "On chain 1, only first transceiver should be enabled for receiving from chain 2" + ); + require(nttManagerChain1.getThreshold() == 2, "On chain 1, the default threshold is wrong"); + require( + nttManagerChain1.getPerChainThreshold(chainId2) == 1, + "On chain 1, threshold for chain 2 is wrong" + ); + + // Verify config of chain two. ////////////////////////////////////////////////////////// + require( + nttManagerChain2.isSendTransceiverEnabledForChain( + address(chainTwoFirstTransceiver), chainId1 + ), + "On chain 2, first transceiver should be enabled for sending to chain 1" + ); + require( + !nttManagerChain2.isSendTransceiverEnabledForChain( + address(chainTwoSecondTransceiver), chainId1 + ), + "On chain 2, second transceiver should be not enabled for sending to chain 1" + ); + require( + nttManagerChain2.getEnabledRecvTransceiversForChain(chainId1) == 0x3, + "On chain 2, both transceivers should be enabled for receiving from chain 1" + ); + require(nttManagerChain2.getThreshold() == 2, "On chain 2, the default threshold is wrong"); + require( + nttManagerChain2.getPerChainThreshold(chainId1) == 2, + "On chain 2, threshold for chain 1 is wrong" + ); + } + + function test_transfer() public { + vm.chainId(chainId1); + + // Setting up the transfer + DummyToken token1 = DummyToken(nttManagerChain1.token()); + DummyToken token2 = DummyTokenMintAndBurn(nttManagerChain2.token()); + + uint8 decimals = token1.decimals(); + uint256 sendingAmount = 5 * 10 ** decimals; + token1.mintDummy(address(userA), 5 * 10 ** decimals); + + // Transfer tokens from chain one to chain two through standard means (not relayer) + vm.startPrank(userA); + token1.approve(address(nttManagerChain1), sendingAmount); + vm.recordLogs(); + { + uint256 nttManagerBalanceBefore = token1.balanceOf(address(nttManagerChain1)); + uint256 userBalanceBefore = token1.balanceOf(address(userA)); + nttManagerChain1.transfer(sendingAmount, chainId2, bytes32(uint256(uint160(userB)))); + + // Balance check on funds going in and out working as expected + uint256 nttManagerBalanceAfter = token1.balanceOf(address(nttManagerChain1)); + uint256 userBalanceAfter = token1.balanceOf(address(userB)); + require( + nttManagerBalanceBefore + sendingAmount == nttManagerBalanceAfter, + "Should be locking the tokens" + ); + require( + userBalanceBefore - sendingAmount == userBalanceAfter, + "User should have sent tokens" + ); + } + + vm.stopPrank(); + + // Get the logs. There should be two messages going from chain 1 to chain 2. + Vm.Log[] memory entries = guardian.fetchWormholeMessageFromLog(vm.getRecordedLogs()); + require(2 == entries.length, "Unexpected number of log entries 1"); + bytes[] memory encodedVMs = new bytes[](entries.length); + for (uint256 i = 0; i < encodedVMs.length; i++) { + encodedVMs[i] = guardian.fetchSignedMessageFromLogs(entries[i], chainId1); + } + + // Chain2 verification and checks + vm.chainId(chainId2); + + { + uint256 supplyBefore = token2.totalSupply(); + + // Receiving the first VAA on the first transceiver shouldn't do anything. + chainTwoFirstTransceiver.receiveMessage(encodedVMs[0]); + uint256 supplyAfter = token2.totalSupply(); + require(supplyBefore == supplyAfter, "It looks like the transfer happened too soon"); + + // Receiving the first VAA on the second transceiver should revert. + vm.expectRevert( + abi.encodeWithSelector( + IWormholeTransceiver.InvalidWormholePeer.selector, + chainId1, + chainOneFirstTransceiver + ) + ); + chainTwoSecondTransceiver.receiveMessage(encodedVMs[0]); + + // Receiving the second VAA on the second one should complete the transfer. + chainTwoSecondTransceiver.receiveMessage(encodedVMs[1]); + supplyAfter = token2.totalSupply(); + + require(sendingAmount + supplyBefore == supplyAfter, "Supplies dont match"); + require(token2.balanceOf(userB) == sendingAmount, "User didn't receive tokens"); + require( + token2.balanceOf(address(nttManagerChain2)) == 0, "NttManager has unintended funds" + ); + } + + // Go back the other way from a THIRD user + vm.prank(userB); + token2.transfer(userC, sendingAmount); + + vm.startPrank(userC); + token2.approve(address(nttManagerChain2), sendingAmount); + vm.recordLogs(); + + // Supply checks on the transfer + { + uint256 supplyBefore = token2.totalSupply(); + nttManagerChain2.transfer( + sendingAmount, + chainId1, + toWormholeFormat(userD), + toWormholeFormat(userC), + false, + encodeTransceiverInstruction(true) + ); + + uint256 supplyAfter = token2.totalSupply(); + + require(sendingAmount - supplyBefore == supplyAfter, "Supplies don't match"); + require(token2.balanceOf(userB) == 0, "OG user receive tokens"); + require(token2.balanceOf(userC) == 0, "Sending user didn't receive tokens"); + require( + token2.balanceOf(address(nttManagerChain2)) == 0, + "NttManager didn't receive unintended funds" + ); + } + + // Get the logs. There should only be one message going from chain 2 to chain 1. + entries = guardian.fetchWormholeMessageFromLog(vm.getRecordedLogs()); + require(1 == entries.length, "Unexpected number of log entries 2"); + encodedVMs = new bytes[](entries.length); + for (uint256 i = 0; i < encodedVMs.length; i++) { + encodedVMs[i] = guardian.fetchSignedMessageFromLogs(entries[i], chainId2); + } + + // Chain1 verification and checks with the receiving of the message + vm.chainId(chainId1); + + { + // Receiving a single VAA should do the trick. + uint256 supplyBefore = token1.totalSupply(); + chainOneFirstTransceiver.receiveMessage(encodedVMs[0]); + uint256 supplyAfter = token1.totalSupply(); + + require(supplyBefore == supplyAfter, "Supplies don't match between operations"); + require(token1.balanceOf(userB) == 0, "OG user receive tokens"); + require(token1.balanceOf(userC) == 0, "Sending user didn't receive tokens"); + require(token1.balanceOf(userD) == sendingAmount, "User received funds"); + } + } + + function encodeTransceiverInstruction( + bool relayer_off + ) public view returns (bytes memory) { + WormholeTransceiver.WormholeTransceiverInstruction memory instruction = + IWormholeTransceiver.WormholeTransceiverInstruction(relayer_off); + bytes memory encodedInstructionWormhole = + chainOneFirstTransceiver.encodeWormholeTransceiverInstruction(instruction); + TransceiverStructs.TransceiverInstruction memory TransceiverInstruction = TransceiverStructs + .TransceiverInstruction({index: 0, payload: encodedInstructionWormhole}); + TransceiverStructs.TransceiverInstruction[] memory TransceiverInstructions = + new TransceiverStructs.TransceiverInstruction[](1); + TransceiverInstructions[0] = TransceiverInstruction; + return TransceiverStructs.encodeTransceiverInstructions(TransceiverInstructions); + } + + // Encode an instruction for each of the relayers + function encodeTransceiverInstructions( + bool relayer_off + ) public view returns (bytes memory) { + WormholeTransceiver.WormholeTransceiverInstruction memory instruction = + IWormholeTransceiver.WormholeTransceiverInstruction(relayer_off); + + bytes memory encodedInstructionWormhole = + chainOneFirstTransceiver.encodeWormholeTransceiverInstruction(instruction); + + TransceiverStructs.TransceiverInstruction memory TransceiverInstruction1 = + TransceiverStructs.TransceiverInstruction({index: 0, payload: encodedInstructionWormhole}); + TransceiverStructs.TransceiverInstruction memory TransceiverInstruction2 = + TransceiverStructs.TransceiverInstruction({index: 1, payload: encodedInstructionWormhole}); + + TransceiverStructs.TransceiverInstruction[] memory TransceiverInstructions = + new TransceiverStructs.TransceiverInstruction[](2); + + TransceiverInstructions[0] = TransceiverInstruction1; + TransceiverInstructions[1] = TransceiverInstruction2; + + return TransceiverStructs.encodeTransceiverInstructions(TransceiverInstructions); + } +} diff --git a/evm/test/mocks/MockNttManager.sol b/evm/test/mocks/MockNttManager.sol index 6c3fd09fe..8586db36f 100644 --- a/evm/test/mocks/MockNttManager.sol +++ b/evm/test/mocks/MockNttManager.sol @@ -111,3 +111,24 @@ contract MockNttManagerStorageLayoutChange is NttManager { c = address(0x3); } } + +contract MockNttManagerNoRateLimitingContractForTest is NttManagerNoRateLimiting { + constructor( + address token, + Mode mode, + uint16 chainId + ) NttManagerNoRateLimiting(token, mode, chainId) {} + + function isSendTransceiverEnabledForChain( + address transceiver, + uint16 forChainId + ) external view returns (bool) { + return _isSendTransceiverEnabledForChain(transceiver, forChainId); + } + + function getEnabledRecvTransceiversForChain( + uint16 forChainId + ) external view returns (uint64 bitmap) { + return _getEnabledRecvTransceiversForChain(forChainId); + } +} From f4b2d76db9c47da87f9a410fcc5bead760f5c36b Mon Sep 17 00:00:00 2001 From: Bruce Riley Date: Wed, 25 Sep 2024 11:17:59 -0500 Subject: [PATCH 05/10] EVM: Per chain transceiver code cleanup --- evm/README.md | 8 + evm/src/NttManager/ManagerBase.sol | 19 +- .../NttManagerWithPerChainTransceivers.sol | 69 ++++-- evm/src/interfaces/IManagerBase.sol | 15 +- evm/test/PerChainTransceivers.t.sol | 226 ++++++++++++++---- 5 files changed, 253 insertions(+), 84 deletions(-) diff --git a/evm/README.md b/evm/README.md index 8888b52b8..e3aab2675 100644 --- a/evm/README.md +++ b/evm/README.md @@ -313,3 +313,11 @@ The `mint` and `setMinter` methods found in the [`INttToken` Interface](src/inte Although the rate limiting feature can be disabled during contract instantiation, it still occupies code space in the `NttManager` contract. If you wish to free up code space for custom development, you can instead instantiate the `NttManagerNoRateLimiting` contract. This contract is built without the bulk of the rate limiting code. Note that the current immutable checks do not allow modifying the rate-limiting parameters during migration. This means migrating from an instance of `NttManager` with rate-limiting enabled to `NttManagerNoRateLimiting` or vice versa is not officially supported. + +#### Per-Chain-Transceivers + +The `NttManagerNoRateLimiting` contract inherits from the `NttManagerWithPerChainTransceivers` abstract contract, which allows configuring different transceivers and thresholds for each chain. +You can configure a different set of send and receive transceivers for each chain, and if you don't specifically enable any transceivers for a chain, then all transceivers will be used for it. + +As an example, you could set up the NTT manager so that for chain1 it sends on transceiverA and transceiverB, but it only expects a response from transceiverA. Additionally, you could enable +two transceivers for receiving but set the threshold to one, meaning only one response (from either transceiver) is required. diff --git a/evm/src/NttManager/ManagerBase.sol b/evm/src/NttManager/ManagerBase.sol index a269660b3..c52c64121 100644 --- a/evm/src/NttManager/ManagerBase.sol +++ b/evm/src/NttManager/ManagerBase.sol @@ -301,9 +301,8 @@ abstract contract ManagerBase is /// @inheritdoc IManagerBase function isMessageApproved( bytes32 digest - ) public view returns (bool) { - uint16 sourceChainId = _getMessageAttestationsStorage()[digest].sourceChainId; - uint8 threshold = getPerChainThreshold(sourceChainId); + ) public view virtual returns (bool) { + uint8 threshold = getThreshold(); return messageAttestations(digest) >= threshold && threshold > 0; } @@ -469,12 +468,10 @@ abstract contract ManagerBase is /// @dev Returns the bitmap of attestations from enabled transceivers for a given message. function _getMessageAttestations( bytes32 digest - ) internal view returns (uint64) { + ) internal view virtual returns (uint64) { uint64 enabledTransceiverBitmap = _getEnabledTransceiversBitmap(); - uint16 sourceChainId = _getMessageAttestationsStorage()[digest].sourceChainId; - uint64 enabledTransceiversForChain = _getEnabledRecvTransceiversForChain(sourceChainId); - return _getMessageAttestationsStorage()[digest].attestedTransceivers - & enabledTransceiverBitmap & enabledTransceiversForChain; + return + _getMessageAttestationsStorage()[digest].attestedTransceivers & enabledTransceiverBitmap; } function _getEnabledTransceiverAttestedToMessage( @@ -512,12 +509,6 @@ abstract contract ManagerBase is return true; } - function _getEnabledRecvTransceiversForChain( - uint16 // forChainId - ) internal view virtual returns (uint64 bitmap) { - return type(uint64).max; - } - /// ============== Invariants ============================================= /// @dev When we add new immutables, this function should be updated diff --git a/evm/src/NttManager/NttManagerWithPerChainTransceivers.sol b/evm/src/NttManager/NttManagerWithPerChainTransceivers.sol index 9d66948c1..fcb3f4467 100644 --- a/evm/src/NttManager/NttManagerWithPerChainTransceivers.sol +++ b/evm/src/NttManager/NttManagerWithPerChainTransceivers.sol @@ -3,13 +3,13 @@ pragma solidity >=0.8.8 <0.9.0; import "./NttManager.sol"; -/// @title NttManagerNoRateLimiting +/// @title NttManagerWithPerChainTransceivers /// @author Wormhole Project Contributors. -/// @notice The NttManagerNoRateLimiting abstract contract is an implementation of +/// @notice The NttManagerWithPerChainTransceivers abstract contract is an implementation of /// NttManager that allows configuring different transceivers and thresholds -/// for each chain. Note that you can configure a different set of send and -/// receive transceivers for each chain, and if you don't specifically enable -/// any transceivers for a chain, then all transceivers will be used for it. +/// for each chain. You can configure a different set of send and receive +/// transceivers for each chain, and if you don't specifically enable any +/// transceivers for a chain, then all transceivers will be used for it. /// /// @dev All of the developer notes from `NttManager` apply here. abstract contract NttManagerWithPerChainTransceivers is NttManager { @@ -21,6 +21,8 @@ abstract contract NttManagerWithPerChainTransceivers is NttManager { bool _skipRateLimiting ) NttManager(_token, _mode, _chainId, _rateLimitDuration, _skipRateLimiting) {} + // ==================== Storage slots ========================================================== + bytes32 private constant SEND_TRANSCEIVER_BITMAP_SLOT = bytes32(uint256(keccak256("nttpct.sendTransceiverBitmap")) - 1); @@ -34,7 +36,8 @@ abstract contract NttManagerWithPerChainTransceivers is NttManager { address transceiver, uint16 forChainId ) external override(ManagerBase, IManagerBase) onlyOwner { - _enableTranceiverForChain(transceiver, forChainId, SEND_TRANSCEIVER_BITMAP_SLOT); + _enableTransceiverForChain(transceiver, forChainId, SEND_TRANSCEIVER_BITMAP_SLOT); + emit SendTransceiverEnabledForChain(transceiver, forChainId); } /// @inheritdoc IManagerBase @@ -42,14 +45,15 @@ abstract contract NttManagerWithPerChainTransceivers is NttManager { address transceiver, uint16 forChainId ) external override(ManagerBase, IManagerBase) onlyOwner { - _enableTranceiverForChain(transceiver, forChainId, RECV_TRANSCEIVER_BITMAP_SLOT); + _enableTransceiverForChain(transceiver, forChainId, RECV_TRANSCEIVER_BITMAP_SLOT); + emit RecvTransceiverEnabledForChain(transceiver, forChainId); } - function _enableTranceiverForChain( + function _enableTransceiverForChain( address transceiver, uint16 forChainId, bytes32 tag - ) internal onlyOwner { + ) private { if (transceiver == address(0)) { revert InvalidTransceiverZeroAddress(); } @@ -63,42 +67,36 @@ abstract contract NttManagerWithPerChainTransceivers is NttManager { mapping(uint16 => _EnabledTransceiverBitmap) storage _bitmaps = _getPerChainTransceiverBitmapStorage(tag); _bitmaps[forChainId].bitmap |= uint64(1 << index); - - emit TransceiverEnabledForChain(transceiver, forChainId); } function _isSendTransceiverEnabledForChain( address transceiver, uint16 forChainId ) internal view override returns (bool) { - uint64 bitmap = - _getPerChainTransceiverBitmapStorage(SEND_TRANSCEIVER_BITMAP_SLOT)[forChainId].bitmap; - if (bitmap == 0) { - // NOTE: this makes it backwards compatible -- if the bitmap is not - // set, it's assumed the corridor uses all transceivers. - bitmap = type(uint64).max; - } + uint64 bitmap = _getPerChainTransceiverBitmap(forChainId, SEND_TRANSCEIVER_BITMAP_SLOT); uint8 index = _getTransceiverInfosStorage()[transceiver].index; return (bitmap & uint64(1 << index)) != 0; } function _getEnabledRecvTransceiversForChain( uint16 forChainId - ) internal view override returns (uint64 bitmap) { - bitmap = - _getPerChainTransceiverBitmapStorage(RECV_TRANSCEIVER_BITMAP_SLOT)[forChainId].bitmap; + ) internal view returns (uint64 bitmap) { + return _getPerChainTransceiverBitmap(forChainId, RECV_TRANSCEIVER_BITMAP_SLOT); + } + + function _getPerChainTransceiverBitmap( + uint16 forChainId, + bytes32 tag + ) private view returns (uint64 bitmap) { + bitmap = _getPerChainTransceiverBitmapStorage(tag)[forChainId].bitmap; if (bitmap == 0) { - // NOTE: this makes it backwards compatible -- if the bitmap is not - // set, it's assumed the corridor uses all transceivers. bitmap = type(uint64).max; } } function _getPerChainTransceiverBitmapStorage( bytes32 tag - ) internal pure returns (mapping(uint16 => _EnabledTransceiverBitmap) storage $) { - // TODO: this is safe (reusing the storage slot, because the mapping - // doesn't write into the slot itself) buy maybe we shouldn't? + ) private pure returns (mapping(uint16 => _EnabledTransceiverBitmap) storage $) { uint256 slot = uint256(tag); assembly ("memory-safe") { $.slot := slot @@ -147,4 +145,23 @@ abstract contract NttManagerWithPerChainTransceivers is NttManager { $.slot := slot } } + + /// @inheritdoc IManagerBase + function isMessageApproved( + bytes32 digest + ) public view override(ManagerBase, IManagerBase) returns (bool) { + uint16 sourceChainId = _getMessageAttestationsStorage()[digest].sourceChainId; + uint8 threshold = getPerChainThreshold(sourceChainId); + return messageAttestations(digest) >= threshold && threshold > 0; + } + + function _getMessageAttestations( + bytes32 digest + ) internal view override returns (uint64) { + AttestationInfo memory attInfo = _getMessageAttestationsStorage()[digest]; + uint64 enabledTransceiverBitmap = _getEnabledTransceiversBitmap(); + uint64 enabledTransceiversForChain = + _getEnabledRecvTransceiversForChain(attInfo.sourceChainId); + return attInfo.attestedTransceivers & enabledTransceiverBitmap & enabledTransceiversForChain; + } } diff --git a/evm/src/interfaces/IManagerBase.sol b/evm/src/interfaces/IManagerBase.sol index 708b2380c..7f5c378ae 100644 --- a/evm/src/interfaces/IManagerBase.sol +++ b/evm/src/interfaces/IManagerBase.sol @@ -51,7 +51,7 @@ interface IManagerBase { /// @notice Emmitted when the per-chain threshold required transceivers is changed. /// @dev Topic0 - /// 0x2a855b929b9a53c6fb5b5ed248b27e502b709c088e036a5aa17620c8fc5085a9. + /// 0x899b344e9e176bdedb9f57f13e014ff93a874404737f35d71dd95a2e858814d8. /// @param chainId The chain to which the threshold applies. /// @param oldThreshold The old threshold. /// @param threshold The new threshold. @@ -65,12 +65,19 @@ interface IManagerBase { /// @param threshold The current threshold of transceivers. event TransceiverAdded(address transceiver, uint256 transceiversNum, uint8 threshold); - /// @notice Emitted when a transceiver is enabled for a chain. + /// @notice Emitted when a transceiver is enabled for sending on a chain. + /// @dev Topic0 + /// 0x8b14d833f2eae4d6fc1d6037cc37fc69ed72e184b586ab5cda3dda94cc4cc37d. + /// @param transceiver The address of the transceiver. + /// @param chainId The chain to which the threshold applies. + event SendTransceiverEnabledForChain(address transceiver, uint16 chainId); + + /// @notice Emitted when a transceiver is enabled for receiving on a chain. /// @dev Topic0 /// 0xf05962b5774c658e85ed80c91a75af9d66d2af2253dda480f90bce78aff5eda5. /// @param transceiver The address of the transceiver. /// @param chainId The chain to which the threshold applies. - event TransceiverEnabledForChain(address transceiver, uint16 chainId); + event RecvTransceiverEnabledForChain(address transceiver, uint16 chainId); /// @notice Emitted when an transceiver is removed from the nttManager. /// @dev Topic0 @@ -128,6 +135,8 @@ interface IManagerBase { error PeerNotRegistered(uint16 chainId); /// @notice Feature is not implemented. + /// @dev Topic0 + /// 0xd6234725c2592490a5b2926ed2315070d2f568d079cb53600cc4c507f13f8289. error NotImplemented(); /// @notice Fetch the delivery price for a given recipient chain transfer. diff --git a/evm/test/PerChainTransceivers.t.sol b/evm/test/PerChainTransceivers.t.sol index 1514ef445..ac9e67582 100755 --- a/evm/test/PerChainTransceivers.t.sol +++ b/evm/test/PerChainTransceivers.t.sol @@ -408,17 +408,13 @@ contract TestPerChainTransceivers is Test, IRateLimiterEvents { // Chain2 verification and checks vm.chainId(chainId2); - { - uint256 supplyBefore = token2.totalSupply(); - wormholeTransceiverChain2.receiveMessage(encodedVMs[0]); - uint256 supplyAfter = token2.totalSupply(); + uint256 supplyBefore = token2.totalSupply(); + wormholeTransceiverChain2.receiveMessage(encodedVMs[0]); + uint256 supplyAfter = token2.totalSupply(); - require(sendingAmount + supplyBefore == supplyAfter, "Supplies dont match"); - require(token2.balanceOf(userB) == sendingAmount, "User didn't receive tokens"); - require( - token2.balanceOf(address(nttManagerChain2)) == 0, "NttManager has unintended funds" - ); - } + require(sendingAmount + supplyBefore == supplyAfter, "Supplies dont match"); + require(token2.balanceOf(userB) == sendingAmount, "User didn't receive tokens"); + require(token2.balanceOf(address(nttManagerChain2)) == 0, "NttManager has unintended funds"); // Go back the other way from a THIRD user vm.prank(userB); @@ -429,27 +425,25 @@ contract TestPerChainTransceivers is Test, IRateLimiterEvents { vm.recordLogs(); // Supply checks on the transfer - { - uint256 supplyBefore = token2.totalSupply(); - nttManagerChain2.transfer( - sendingAmount, - chainId1, - toWormholeFormat(userD), - toWormholeFormat(userC), - false, - encodeTransceiverInstruction(true) - ); + supplyBefore = token2.totalSupply(); + nttManagerChain2.transfer( + sendingAmount, + chainId1, + toWormholeFormat(userD), + toWormholeFormat(userC), + false, + encodeTransceiverInstruction(true) + ); - uint256 supplyAfter = token2.totalSupply(); + supplyAfter = token2.totalSupply(); - require(sendingAmount - supplyBefore == supplyAfter, "Supplies don't match"); - require(token2.balanceOf(userB) == 0, "OG user receive tokens"); - require(token2.balanceOf(userC) == 0, "Sending user didn't receive tokens"); - require( - token2.balanceOf(address(nttManagerChain2)) == 0, - "NttManager didn't receive unintended funds" - ); - } + require(sendingAmount - supplyBefore == supplyAfter, "Supplies don't match"); + require(token2.balanceOf(userB) == 0, "OG user receive tokens"); + require(token2.balanceOf(userC) == 0, "Sending user didn't receive tokens"); + require( + token2.balanceOf(address(nttManagerChain2)) == 0, + "NttManager didn't receive unintended funds" + ); // Get and sign the log to go down the other pipe. Thank you to whoever wrote this code in the past! entries = guardian.fetchWormholeMessageFromLog(vm.getRecordedLogs()); @@ -462,18 +456,27 @@ contract TestPerChainTransceivers is Test, IRateLimiterEvents { // Chain1 verification and checks with the receiving of the message vm.chainId(chainId1); - { - uint256 supplyBefore = token1.totalSupply(); - wormholeTransceiverChain1.receiveMessage(encodedVMs[0]); - uint256 supplyAfter = token1.totalSupply(); - - require(supplyBefore == supplyAfter, "Supplies don't match between operations"); - require(token1.balanceOf(userB) == 0, "OG user receive tokens"); - require(token1.balanceOf(userC) == 0, "Sending user didn't receive tokens"); - require(token1.balanceOf(userD) == sendingAmount, "User received funds"); - } + supplyBefore = token1.totalSupply(); + wormholeTransceiverChain1.receiveMessage(encodedVMs[0]); + supplyAfter = token1.totalSupply(); - //////////////////////// receive message 1. + require(supplyBefore == supplyAfter, "Supplies don't match between operations"); + require(token1.balanceOf(userB) == 0, "OG user receive tokens"); + require(token1.balanceOf(userC) == 0, "Sending user didn't receive tokens"); + require(token1.balanceOf(userD) == sendingAmount, "Transfer did not complete"); + + // Submitting the second message back on chain one should not change anything. + supplyBefore = token1.totalSupply(); + secondWormholeTransceiverChain1.receiveMessage(encodedVMs[1]); + supplyAfter = token1.totalSupply(); + + require(supplyBefore == supplyAfter, "Supplies don't match between operations"); + require(token1.balanceOf(userB) == 0, "OG user receive tokens"); + require(token1.balanceOf(userC) == 0, "Sending user didn't receive tokens"); + require( + token1.balanceOf(userD) == sendingAmount, + "Second message updated the balance when it shouldn't have" + ); } // This test does a transfer between chain one and chain three. @@ -598,12 +601,153 @@ contract TestPerChainTransceivers is Test, IRateLimiterEvents { secondWormholeTransceiverChain1.receiveMessage(encodedVMs[1]); supplyAfter = token1.totalSupply(); - require(supplyBefore == supplyAfter, "Supplies don't match between operations"); /////////////////// Is this right?? + require(supplyBefore == supplyAfter, "Supplies don't match between operations"); require(token1.balanceOf(userB) == 0, "OG user receive tokens"); require(token1.balanceOf(userC) == 0, "Sending user didn't receive tokens"); require(token1.balanceOf(userD) == sendingAmount, "User received funds"); } + // This test does a transfer between chain one and chain three. + // It sets the threshold for chain three on chain one to one, so the transfer should complete when the first VAA is posted. + function test_thresholdLessThanNumReceivers() public { + nttManagerChain1.setPerChainThreshold(chainId3, 1); + require( + nttManagerChain1.getPerChainThreshold(chainId3) == 1, + "Failed to set per-chain threshold" + ); + + vm.chainId(chainId1); + + // Setting up the transfer + DummyToken token1 = DummyToken(nttManagerChain1.token()); + DummyToken token3 = DummyTokenMintAndBurn(nttManagerChain3.token()); + + uint8 decimals = token1.decimals(); + uint256 sendingAmount = 5 * 10 ** decimals; + token1.mintDummy(address(userA), 5 * 10 ** decimals); + vm.startPrank(userA); + token1.approve(address(nttManagerChain1), sendingAmount); + + vm.recordLogs(); + + // Send token from chain 1 to chain 3, userB. + { + uint256 nttManagerBalanceBefore = token1.balanceOf(address(nttManagerChain1)); + uint256 userBalanceBefore = token1.balanceOf(address(userA)); + nttManagerChain1.transfer(sendingAmount, chainId3, bytes32(uint256(uint160(userB)))); + + // Balance check on funds going in and out working as expected + uint256 nttManagerBalanceAfter = token1.balanceOf(address(nttManagerChain1)); + uint256 userBalanceAfter = token1.balanceOf(address(userB)); + require( + nttManagerBalanceBefore + sendingAmount == nttManagerBalanceAfter, + "Should be locking the tokens" + ); + require( + userBalanceBefore - sendingAmount == userBalanceAfter, + "User should have sent tokens" + ); + } + + vm.stopPrank(); + + // Get and sign the log to go down the other pipes. There should be two messages since we have two transceivers. + Vm.Log[] memory entries = guardian.fetchWormholeMessageFromLog(vm.getRecordedLogs()); + require(2 == entries.length, "Unexpected number of log entries 3"); + bytes[] memory encodedVMs = new bytes[](entries.length); + for (uint256 i = 0; i < encodedVMs.length; i++) { + encodedVMs[i] = guardian.fetchSignedMessageFromLogs(entries[i], chainId1); + } + + // Chain3 verification and checks + vm.chainId(chainId3); + + uint256 supplyBefore = token3.totalSupply(); + + // Submit the first message on chain 3. The numbers shouldn't change yet since the threshold is two. + wormholeTransceiverChain3.receiveMessage(encodedVMs[0]); + uint256 supplyAfter = token3.totalSupply(); + + require(supplyBefore == supplyAfter, "Supplies changed early"); + require(token3.balanceOf(userB) == 0, "User receive tokens early"); + require(token3.balanceOf(address(nttManagerChain3)) == 0, "NttManager has unintended funds"); + + // Submit the second message and the transfer should complete. + secondWormholeTransceiverChain3.receiveMessage(encodedVMs[1]); + supplyAfter = token3.totalSupply(); + + require(sendingAmount + supplyBefore == supplyAfter, "Supplies dont match"); + require(token3.balanceOf(userB) == sendingAmount, "User didn't receive tokens"); + require(token3.balanceOf(address(nttManagerChain3)) == 0, "NttManager has unintended funds"); + + // Go back the other way from a THIRD user + vm.prank(userB); + token3.transfer(userC, sendingAmount); + + vm.startPrank(userC); + token3.approve(address(nttManagerChain3), sendingAmount); + vm.recordLogs(); + + // Supply checks on the transfer + supplyBefore = token3.totalSupply(); + nttManagerChain3.transfer( + sendingAmount, + chainId1, + toWormholeFormat(userD), + toWormholeFormat(userC), + false, + encodeTransceiverInstruction(true) + ); + + supplyAfter = token3.totalSupply(); + + require(sendingAmount - supplyBefore == supplyAfter, "Supplies don't match"); + require(token3.balanceOf(userB) == 0, "OG user receive tokens"); + require(token3.balanceOf(userC) == 0, "Sending user didn't receive tokens"); + require( + token3.balanceOf(address(nttManagerChain3)) == 0, + "NttManager didn't receive unintended funds" + ); + + // Get and sign the log to go down the other pipe. Thank you to whoever wrote this code in the past! + entries = guardian.fetchWormholeMessageFromLog(vm.getRecordedLogs()); + require(2 == entries.length, "Unexpected number of log entries for response"); + encodedVMs = new bytes[](entries.length); + for (uint256 i = 0; i < encodedVMs.length; i++) { + encodedVMs[i] = guardian.fetchSignedMessageFromLogs(entries[i], chainId3); + } + + // Chain1 verification and checks with the receiving of the message + vm.chainId(chainId1); + + // Submit the first message back on chain one. Since the threshold from chain three is one, the numbers should update. + // Just for fun, we are having the second transceiver receive the message first. The order doesn't matter. + supplyBefore = token1.totalSupply(); + secondWormholeTransceiverChain1.receiveMessage(encodedVMs[1]); + supplyAfter = token1.totalSupply(); + + require(supplyBefore == supplyAfter, "Supplies don't match between operations"); + require(token1.balanceOf(userB) == 0, "OG user receive tokens"); + require(token1.balanceOf(userC) == 0, "Sending user didn't receive tokens"); + require( + token1.balanceOf(userD) == sendingAmount, + "Transfer did not complete after first message" + ); + + // Submitting the second message back on chain one should not change anything. + supplyBefore = token1.totalSupply(); + wormholeTransceiverChain1.receiveMessage(encodedVMs[0]); + supplyAfter = token1.totalSupply(); + + require(supplyBefore == supplyAfter, "Supplies don't match between operations"); + require(token1.balanceOf(userB) == 0, "OG user receive tokens"); + require(token1.balanceOf(userC) == 0, "Sending user didn't receive tokens"); + require( + token1.balanceOf(userD) == sendingAmount, + "Second message updated the balance when it shouldn't have" + ); + } + function encodeTransceiverInstruction( bool relayer_off ) public view returns (bytes memory) { From f6f994d080366197541804ee72926acd996fda95 Mon Sep 17 00:00:00 2001 From: Bruce Riley Date: Fri, 27 Sep 2024 10:10:45 -0500 Subject: [PATCH 06/10] EVM: Flip contract hierarchy for NttManagerWithPerChainTransceivers --- evm/README.md | 6 +++--- .../NttManager/NttManagerNoRateLimiting.sol | 6 +++--- .../NttManagerWithPerChainTransceivers.sol | 10 ++++------ evm/test/PerChainTransceivers.t.sol | 18 +++++++++--------- evm/test/PerChainTransceiversDemo.t.sol | 12 ++++++------ evm/test/mocks/MockNttManager.sol | 9 +++++++++ 6 files changed, 34 insertions(+), 27 deletions(-) diff --git a/evm/README.md b/evm/README.md index e3aab2675..3ddb0f2e0 100644 --- a/evm/README.md +++ b/evm/README.md @@ -316,8 +316,8 @@ from an instance of `NttManager` with rate-limiting enabled to `NttManagerNoRate #### Per-Chain-Transceivers -The `NttManagerNoRateLimiting` contract inherits from the `NttManagerWithPerChainTransceivers` abstract contract, which allows configuring different transceivers and thresholds for each chain. -You can configure a different set of send and receive transceivers for each chain, and if you don't specifically enable any transceivers for a chain, then all transceivers will be used for it. +The `NttManagerWithPerChainTransceivers` contract which inherits from `NttManagerNoRateLimiting` and allows configuring different transceivers and thresholds for each chain. +You can configure a different set of send and receive transceivers for each chain. If you don't specifically enable any transceivers for a chain, then all transceivers will be used. -As an example, you could set up the NTT manager so that for chain1 it sends on transceiverA and transceiverB, but it only expects a response from transceiverA. Additionally, you could enable +As an example, you could set up the manager so that for chain1 it sends on transceiverA and transceiverB, but it only expects a response from transceiverA. Additionally, you could enable two transceivers for receiving but set the threshold to one, meaning only one response (from either transceiver) is required. diff --git a/evm/src/NttManager/NttManagerNoRateLimiting.sol b/evm/src/NttManager/NttManagerNoRateLimiting.sol index 76c84f336..82358c5e8 100644 --- a/evm/src/NttManager/NttManagerNoRateLimiting.sol +++ b/evm/src/NttManager/NttManagerNoRateLimiting.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache 2 pragma solidity >=0.8.8 <0.9.0; -import "./NttManagerWithPerChainTransceivers.sol"; +import "./NttManager.sol"; /// @title NttManagerNoRateLimiting /// @author Wormhole Project Contributors. @@ -10,12 +10,12 @@ import "./NttManagerWithPerChainTransceivers.sol"; /// free up code space. /// /// @dev All of the developer notes from `NttManager` apply here. -contract NttManagerNoRateLimiting is NttManagerWithPerChainTransceivers { +contract NttManagerNoRateLimiting is NttManager { constructor( address _token, Mode _mode, uint16 _chainId - ) NttManagerWithPerChainTransceivers(_token, _mode, _chainId, 0, true) {} + ) NttManager(_token, _mode, _chainId, 0, true) {} // ==================== Override RateLimiter functions ========================= diff --git a/evm/src/NttManager/NttManagerWithPerChainTransceivers.sol b/evm/src/NttManager/NttManagerWithPerChainTransceivers.sol index fcb3f4467..af293c0dc 100644 --- a/evm/src/NttManager/NttManagerWithPerChainTransceivers.sol +++ b/evm/src/NttManager/NttManagerWithPerChainTransceivers.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache 2 pragma solidity >=0.8.8 <0.9.0; -import "./NttManager.sol"; +import "./NttManagerNoRateLimiting.sol"; /// @title NttManagerWithPerChainTransceivers /// @author Wormhole Project Contributors. @@ -12,14 +12,12 @@ import "./NttManager.sol"; /// transceivers for a chain, then all transceivers will be used for it. /// /// @dev All of the developer notes from `NttManager` apply here. -abstract contract NttManagerWithPerChainTransceivers is NttManager { +contract NttManagerWithPerChainTransceivers is NttManagerNoRateLimiting { constructor( address _token, Mode _mode, - uint16 _chainId, - uint64 _rateLimitDuration, - bool _skipRateLimiting - ) NttManager(_token, _mode, _chainId, _rateLimitDuration, _skipRateLimiting) {} + uint16 _chainId + ) NttManagerNoRateLimiting(_token, _mode, _chainId) {} // ==================== Storage slots ========================================================== diff --git a/evm/test/PerChainTransceivers.t.sol b/evm/test/PerChainTransceivers.t.sol index ac9e67582..646c028f9 100755 --- a/evm/test/PerChainTransceivers.t.sol +++ b/evm/test/PerChainTransceivers.t.sol @@ -28,9 +28,9 @@ import "wormhole-solidity-sdk/Utils.sol"; //import "wormhole-solidity-sdk/testing/WormholeRelayerTest.sol"; contract TestPerChainTransceivers is Test, IRateLimiterEvents { - MockNttManagerNoRateLimitingContractForTest nttManagerChain1; - NttManagerNoRateLimiting nttManagerChain2; - NttManagerNoRateLimiting nttManagerChain3; + MockNttManagerWithPerChainTransceivers nttManagerChain1; + NttManagerWithPerChainTransceivers nttManagerChain2; + NttManagerWithPerChainTransceivers nttManagerChain3; using TrimmedAmountLib for uint256; using TrimmedAmountLib for TrimmedAmount; @@ -76,11 +76,11 @@ contract TestPerChainTransceivers is Test, IRateLimiterEvents { vm.chainId(chainId1); DummyToken t1 = new DummyToken(); - NttManager implementation = new MockNttManagerNoRateLimitingContractForTest( + NttManager implementation = new MockNttManagerWithPerChainTransceivers( address(t1), IManagerBase.Mode.LOCKING, chainId1 ); - nttManagerChain1 = MockNttManagerNoRateLimitingContractForTest( + nttManagerChain1 = MockNttManagerWithPerChainTransceivers( address(new ERC1967Proxy(address(implementation), "")) ); nttManagerChain1.initialize(); @@ -129,11 +129,11 @@ contract TestPerChainTransceivers is Test, IRateLimiterEvents { // Chain 2 setup vm.chainId(chainId2); DummyToken t2 = new DummyTokenMintAndBurn(); - NttManager implementationChain2 = new MockNttManagerNoRateLimitingContract( + NttManager implementationChain2 = new MockNttManagerWithPerChainTransceivers( address(t2), IManagerBase.Mode.BURNING, chainId2 ); - nttManagerChain2 = MockNttManagerNoRateLimitingContract( + nttManagerChain2 = MockNttManagerWithPerChainTransceivers( address(new ERC1967Proxy(address(implementationChain2), "")) ); nttManagerChain2.initialize(); @@ -194,11 +194,11 @@ contract TestPerChainTransceivers is Test, IRateLimiterEvents { // Chain 3 setup vm.chainId(chainId3); DummyToken t3 = new DummyTokenMintAndBurn(); - NttManager implementationChain3 = new MockNttManagerNoRateLimitingContract( + NttManager implementationChain3 = new MockNttManagerWithPerChainTransceivers( address(t3), IManagerBase.Mode.BURNING, chainId3 ); - nttManagerChain3 = MockNttManagerNoRateLimitingContract( + nttManagerChain3 = MockNttManagerWithPerChainTransceivers( address(new ERC1967Proxy(address(implementationChain3), "")) ); nttManagerChain3.initialize(); diff --git a/evm/test/PerChainTransceiversDemo.t.sol b/evm/test/PerChainTransceiversDemo.t.sol index eb7f575b5..9c0f89227 100755 --- a/evm/test/PerChainTransceiversDemo.t.sol +++ b/evm/test/PerChainTransceiversDemo.t.sol @@ -41,8 +41,8 @@ import "wormhole-solidity-sdk/Utils.sol"; // with the appropriate thresholds. We will then do a transfer. contract TestPerChainTransceiversDemo is Test, IRateLimiterEvents { - MockNttManagerNoRateLimitingContractForTest nttManagerChain1; - MockNttManagerNoRateLimitingContractForTest nttManagerChain2; + MockNttManagerWithPerChainTransceivers nttManagerChain1; + MockNttManagerWithPerChainTransceivers nttManagerChain2; using TrimmedAmountLib for uint256; using TrimmedAmountLib for TrimmedAmount; @@ -81,11 +81,11 @@ contract TestPerChainTransceiversDemo is Test, IRateLimiterEvents { // Set up the manager on chain one. ////////////////////////////////////////////////////////////////// vm.chainId(chainId1); DummyToken t1 = new DummyToken(); - NttManager implementation = new MockNttManagerNoRateLimitingContractForTest( + NttManager implementation = new MockNttManagerWithPerChainTransceivers( address(t1), IManagerBase.Mode.LOCKING, chainId1 ); - nttManagerChain1 = MockNttManagerNoRateLimitingContractForTest( + nttManagerChain1 = MockNttManagerWithPerChainTransceivers( address(new ERC1967Proxy(address(implementation), "")) ); nttManagerChain1.initialize(); @@ -125,11 +125,11 @@ contract TestPerChainTransceiversDemo is Test, IRateLimiterEvents { // Set up the manager on chain two. ////////////////////////////////////////////////////////////////// vm.chainId(chainId2); DummyToken t2 = new DummyTokenMintAndBurn(); - NttManager implementationChain2 = new MockNttManagerNoRateLimitingContractForTest( + NttManager implementationChain2 = new MockNttManagerWithPerChainTransceivers( address(t2), IManagerBase.Mode.BURNING, chainId2 ); - nttManagerChain2 = MockNttManagerNoRateLimitingContractForTest( + nttManagerChain2 = MockNttManagerWithPerChainTransceivers( address(new ERC1967Proxy(address(implementationChain2), "")) ); nttManagerChain2.initialize(); diff --git a/evm/test/mocks/MockNttManager.sol b/evm/test/mocks/MockNttManager.sol index 8586db36f..1c988e438 100644 --- a/evm/test/mocks/MockNttManager.sol +++ b/evm/test/mocks/MockNttManager.sol @@ -4,6 +4,7 @@ pragma solidity >=0.8.8 <0.9.0; import "../../src/NttManager/NttManager.sol"; import "../../src/NttManager/NttManagerNoRateLimiting.sol"; +import "../../src/NttManager/NttManagerWithPerChainTransceivers.sol"; contract MockNttManagerContract is NttManager { constructor( @@ -118,6 +119,14 @@ contract MockNttManagerNoRateLimitingContractForTest is NttManagerNoRateLimiting Mode mode, uint16 chainId ) NttManagerNoRateLimiting(token, mode, chainId) {} +} + +contract MockNttManagerWithPerChainTransceivers is NttManagerWithPerChainTransceivers { + constructor( + address token, + Mode mode, + uint16 chainId + ) NttManagerWithPerChainTransceivers(token, mode, chainId) {} function isSendTransceiverEnabledForChain( address transceiver, From f218c1e5d285b3a45683a5c1a844ee8fa8f1823e Mon Sep 17 00:00:00 2001 From: Bruce Riley Date: Sat, 28 Sep 2024 11:34:19 -0500 Subject: [PATCH 07/10] EVM: NttManagerWithPerChainTransceivers review rework --- evm/src/NttManager/ManagerBase.sol | 2 +- .../NttManagerWithPerChainTransceivers.sol | 26 ++++++++++++++++--- evm/src/interfaces/IManagerBase.sol | 8 +++--- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/evm/src/NttManager/ManagerBase.sol b/evm/src/NttManager/ManagerBase.sol index c52c64121..b8a974167 100644 --- a/evm/src/NttManager/ManagerBase.sol +++ b/evm/src/NttManager/ManagerBase.sol @@ -532,7 +532,7 @@ abstract contract ManagerBase is function _checkThresholdInvariants( uint8 threshold - ) internal pure { + ) internal view { _NumTransceivers memory numTransceivers = _getNumTransceiversStorage(); // invariant: threshold <= enabledTransceivers.length diff --git a/evm/src/NttManager/NttManagerWithPerChainTransceivers.sol b/evm/src/NttManager/NttManagerWithPerChainTransceivers.sol index af293c0dc..0a37fc3d3 100644 --- a/evm/src/NttManager/NttManagerWithPerChainTransceivers.sol +++ b/evm/src/NttManager/NttManagerWithPerChainTransceivers.sol @@ -44,6 +44,12 @@ contract NttManagerWithPerChainTransceivers is NttManagerNoRateLimiting { uint16 forChainId ) external override(ManagerBase, IManagerBase) onlyOwner { _enableTransceiverForChain(transceiver, forChainId, RECV_TRANSCEIVER_BITMAP_SLOT); + + // Make sure the threshold is set to something. + if (getPerChainThreshold(forChainId) == 0) { + this.setPerChainThreshold(forChainId, 1); + } + emit RecvTransceiverEnabledForChain(transceiver, forChainId); } @@ -56,15 +62,15 @@ contract NttManagerWithPerChainTransceivers is NttManagerNoRateLimiting { revert InvalidTransceiverZeroAddress(); } - mapping(address => TransceiverInfo) storage transceiverInfos = _getTransceiverInfosStorage(); - if (!transceiverInfos[transceiver].registered) { + TransceiverInfo storage transceiverInfo = _getTransceiverInfosStorage()[transceiver]; + + if (!transceiverInfo.registered) { revert NonRegisteredTransceiver(transceiver); } - uint8 index = _getTransceiverInfosStorage()[transceiver].index; mapping(uint16 => _EnabledTransceiverBitmap) storage _bitmaps = _getPerChainTransceiverBitmapStorage(tag); - _bitmaps[forChainId].bitmap |= uint64(1 << index); + _bitmaps[forChainId].bitmap |= uint64(1 << transceiverInfo.index); } function _isSendTransceiverEnabledForChain( @@ -88,6 +94,9 @@ contract NttManagerWithPerChainTransceivers is NttManagerNoRateLimiting { ) private view returns (uint64 bitmap) { bitmap = _getPerChainTransceiverBitmapStorage(tag)[forChainId].bitmap; if (bitmap == 0) { + // Avoid an extra storage slot read and just return all ones. This works + // because the value gets combined with the enabled transceivers on the + // receive side and the specific transceiver index on the send side. bitmap = type(uint64).max; } } @@ -112,6 +121,15 @@ contract NttManagerWithPerChainTransceivers is NttManagerNoRateLimiting { revert ZeroThreshold(); } + // They can't set the threshold greater than the number of enabled transceivers. + uint64 enabledTransceiverBitmap = _getEnabledTransceiversBitmap(); + uint64 enabledTransceiversForChain = _getEnabledRecvTransceiversForChain(forChainId); + uint8 numEnabledTransceivers = + countSetBits(enabledTransceiverBitmap & enabledTransceiversForChain); + if (threshold > numEnabledTransceivers) { + revert ThresholdTooHigh(threshold, numEnabledTransceivers); + } + mapping(uint16 => _Threshold) storage _threshold = _getThresholdStoragePerChain(); uint8 oldThreshold = _threshold[forChainId].num; diff --git a/evm/src/interfaces/IManagerBase.sol b/evm/src/interfaces/IManagerBase.sol index 7f5c378ae..bfd08a184 100644 --- a/evm/src/interfaces/IManagerBase.sol +++ b/evm/src/interfaces/IManagerBase.sol @@ -69,14 +69,14 @@ interface IManagerBase { /// @dev Topic0 /// 0x8b14d833f2eae4d6fc1d6037cc37fc69ed72e184b586ab5cda3dda94cc4cc37d. /// @param transceiver The address of the transceiver. - /// @param chainId The chain to which the threshold applies. + /// @param chainId The chain to which the send applies. event SendTransceiverEnabledForChain(address transceiver, uint16 chainId); /// @notice Emitted when a transceiver is enabled for receiving on a chain. /// @dev Topic0 /// 0xf05962b5774c658e85ed80c91a75af9d66d2af2253dda480f90bce78aff5eda5. /// @param transceiver The address of the transceiver. - /// @param chainId The chain to which the threshold applies. + /// @param chainId The chain to which the receive applies. event RecvTransceiverEnabledForChain(address transceiver, uint16 chainId); /// @notice Emitted when an transceiver is removed from the nttManager. @@ -135,8 +135,8 @@ interface IManagerBase { error PeerNotRegistered(uint16 chainId); /// @notice Feature is not implemented. - /// @dev Topic0 - /// 0xd6234725c2592490a5b2926ed2315070d2f568d079cb53600cc4c507f13f8289. + /// @dev Selector + /// 0xd6234725 error NotImplemented(); /// @notice Fetch the delivery price for a given recipient chain transfer. From c5b58b3aeaa0d77305d267b373acf3ac15dbbe09 Mon Sep 17 00:00:00 2001 From: Bruce Riley Date: Tue, 1 Oct 2024 15:23:05 -0500 Subject: [PATCH 08/10] EVM: Per-Chain Transceivers no defaults --- evm/README.md | 4 +- evm/src/NttManager/ManagerBase.sol | 77 +-- .../NttManagerWithPerChainTransceivers.sol | 323 +++++++++---- evm/src/interfaces/IManagerBase.sol | 122 +++-- evm/test/PerChainTransceivers.t.sol | 451 ++++++++++-------- evm/test/PerChainTransceiversDemo.t.sol | 30 +- 6 files changed, 621 insertions(+), 386 deletions(-) diff --git a/evm/README.md b/evm/README.md index 3ddb0f2e0..89b3ae611 100644 --- a/evm/README.md +++ b/evm/README.md @@ -316,8 +316,8 @@ from an instance of `NttManager` with rate-limiting enabled to `NttManagerNoRate #### Per-Chain-Transceivers -The `NttManagerWithPerChainTransceivers` contract which inherits from `NttManagerNoRateLimiting` and allows configuring different transceivers and thresholds for each chain. -You can configure a different set of send and receive transceivers for each chain. If you don't specifically enable any transceivers for a chain, then all transceivers will be used. +The `NttManagerWithPerChainTransceivers` contract inherits from `NttManagerNoRateLimiting` and allows configuring different transceivers and thresholds for each chain. +You can configure a different set of send and receive transceivers for each chain, as well as the receive threshold for the chain. As an example, you could set up the manager so that for chain1 it sends on transceiverA and transceiverB, but it only expects a response from transceiverA. Additionally, you could enable two transceivers for receiving but set the threshold to one, meaning only one response (from either transceiver) is required. diff --git a/evm/src/NttManager/ManagerBase.sol b/evm/src/NttManager/ManagerBase.sol index b8a974167..f303190c2 100644 --- a/evm/src/NttManager/ManagerBase.sol +++ b/evm/src/NttManager/ManagerBase.sol @@ -291,13 +291,6 @@ abstract contract ManagerBase is return _getThresholdStorage().num; } - /// @inheritdoc IManagerBase - function getPerChainThreshold( - uint16 // forChainId - ) public view virtual returns (uint8) { - return _getThresholdStorage().num; - } - /// @inheritdoc IManagerBase function isMessageApproved( bytes32 digest @@ -410,22 +403,6 @@ abstract contract ManagerBase is _checkThresholdInvariants(); } - /// @inheritdoc IManagerBase - function enableSendTransceiverForChain( - address, // transceiver, - uint16 // forChainId - ) external virtual onlyOwner { - revert NotImplemented(); - } - - /// @inheritdoc IManagerBase - function enableRecvTransceiverForChain( - address, // transceiver, - uint16 // forChainId - ) external virtual onlyOwner { - revert NotImplemented(); - } - /// @inheritdoc IManagerBase function setThreshold( uint8 threshold @@ -444,13 +421,56 @@ abstract contract ManagerBase is } /// @inheritdoc IManagerBase - function setPerChainThreshold( - uint16, // forChainId, + function getSendTransceiverBitmapForChain( + uint16 // forChainId + ) external view virtual returns (uint64) { + return 0; + } + + /// @inheritdoc IManagerBase + function getRecvTransceiverBitmapForChain( + uint16 // forChainId + ) external view virtual returns (uint64) { + return 0; + } + + /// @inheritdoc IManagerBase + function getThresholdForChain( + uint16 // forChainId + ) external view virtual returns (uint8) { + return 0; + } + + /// @inheritdoc IManagerBase + function getChainsEnabledForSending() external view virtual returns (uint16[] memory) {} + + /// @inheritdoc IManagerBase + function getChainsEnabledForReceiving() external view virtual returns (uint16[] memory) {} + + /// @inheritdoc IManagerBase + function setSendTransceiverBitmapForChain( + uint16, // forChainId + uint64 // indexBitmap + ) external virtual onlyOwner { + revert NotImplemented(); + } + + /// @inheritdoc IManagerBase + function setRecvTransceiverBitmapForChain( + uint16, // forChainId + uint64, // indexBitmap uint8 // threshold ) external virtual onlyOwner { revert NotImplemented(); } + /// @inheritdoc IManagerBase + function setTransceiversForChains( + SetTransceiversForChainEntry[] memory // params + ) external virtual onlyOwner { + revert NotImplemented(); + } + // =============== Internal ============================================================== function _setTransceiverAttestedToMessage(bytes32 digest, uint8 index) internal { @@ -527,12 +547,7 @@ abstract contract ManagerBase is } function _checkThresholdInvariants() internal view { - _checkThresholdInvariants(_getThresholdStorage().num); - } - - function _checkThresholdInvariants( - uint8 threshold - ) internal view { + uint8 threshold = _getThresholdStorage().num; _NumTransceivers memory numTransceivers = _getNumTransceiversStorage(); // invariant: threshold <= enabledTransceivers.length diff --git a/evm/src/NttManager/NttManagerWithPerChainTransceivers.sol b/evm/src/NttManager/NttManagerWithPerChainTransceivers.sol index 0a37fc3d3..9be45a5aa 100644 --- a/evm/src/NttManager/NttManagerWithPerChainTransceivers.sol +++ b/evm/src/NttManager/NttManagerWithPerChainTransceivers.sol @@ -5,14 +5,39 @@ import "./NttManagerNoRateLimiting.sol"; /// @title NttManagerWithPerChainTransceivers /// @author Wormhole Project Contributors. -/// @notice The NttManagerWithPerChainTransceivers abstract contract is an implementation of +/// @notice The NttManagerWithPerChainTransceivers contract is an implementation of /// NttManager that allows configuring different transceivers and thresholds -/// for each chain. You can configure a different set of send and receive -/// transceivers for each chain, and if you don't specifically enable any -/// transceivers for a chain, then all transceivers will be used for it. +/// for each chain. You must configure the set of send and receive transceivers +/// for each chain you intend to use. Additionally, you must set the receive +/// threshold for each chain. You can disable a chain by resetting its bitmaps +/// and threshold to zero. /// /// @dev All of the developer notes from `NttManager` apply here. contract NttManagerWithPerChainTransceivers is NttManagerNoRateLimiting { + /// @notice Transceiver index is greater than the number of enabled transceivers. + /// @dev Selector 0x770c2d3c. + /// @param index The transceiver index that is invalid. + /// @param len The length of the transceiver list. + error TransceiverIndexTooLarge(uint8 index, uint256 len); + + /// @notice Transceiver index does not match the one in the list. + /// @dev Selector 0x2f52d3e. + /// @param index The transceiver index that is invalid. + /// @param expectedIndex The index in the transceiver list. + error InvalidTransceiverIndex(uint8 index, uint8 expectedIndex); + + /// @notice Transceiver with specified index is not registered. + /// @dev Selector 0x38ab702a. + /// @param index The index of the transceiver that is not registered. + /// @param transceiver The address of the transceiver that is not registered. + error TransceiverNotRegistered(uint8 index, address transceiver); + + /// @notice Transceiver with specified index is not enabled. + /// @dev Selector 0xcc4cba2a. + /// @param index The index of the transceiver that is not enabled. + /// @param transceiver The address of the transceiver that is not enabled. + error TransceiverNotEnabled(uint8 index, address transceiver); + constructor( address _token, Mode _mode, @@ -27,52 +52,132 @@ contract NttManagerWithPerChainTransceivers is NttManagerNoRateLimiting { bytes32 private constant RECV_TRANSCEIVER_BITMAP_SLOT = bytes32(uint256(keccak256("nttpct.recvTransceiverBitmap")) - 1); - // ==================== Override / implementation of transceiver stuff ========================= + bytes32 private constant SEND_ENABLED_CHAINS_SLOT = + bytes32(uint256(keccak256("nttpct.sendEnabledChains")) - 1); + + bytes32 private constant RECV_ENABLED_CHAINS_SLOT = + bytes32(uint256(keccak256("nttpct.recvEnabledChains")) - 1); + + // =============== Public Getters ======================================================== /// @inheritdoc IManagerBase - function enableSendTransceiverForChain( - address transceiver, + function getSendTransceiverBitmapForChain( uint16 forChainId - ) external override(ManagerBase, IManagerBase) onlyOwner { - _enableTransceiverForChain(transceiver, forChainId, SEND_TRANSCEIVER_BITMAP_SLOT); - emit SendTransceiverEnabledForChain(transceiver, forChainId); + ) external view override(ManagerBase, IManagerBase) returns (uint64) { + return _getPerChainTransceiverBitmap(forChainId, SEND_TRANSCEIVER_BITMAP_SLOT); } /// @inheritdoc IManagerBase - function enableRecvTransceiverForChain( - address transceiver, + function getRecvTransceiverBitmapForChain( uint16 forChainId - ) external override(ManagerBase, IManagerBase) onlyOwner { - _enableTransceiverForChain(transceiver, forChainId, RECV_TRANSCEIVER_BITMAP_SLOT); + ) external view override(ManagerBase, IManagerBase) returns (uint64) { + return _getPerChainTransceiverBitmap(forChainId, RECV_TRANSCEIVER_BITMAP_SLOT); + } - // Make sure the threshold is set to something. - if (getPerChainThreshold(forChainId) == 0) { - this.setPerChainThreshold(forChainId, 1); - } + /// @inheritdoc IManagerBase + function getChainsEnabledForSending() + external + pure + override(ManagerBase, IManagerBase) + returns (uint16[] memory) + { + return _getEnabledChainsStorage(SEND_ENABLED_CHAINS_SLOT); + } - emit RecvTransceiverEnabledForChain(transceiver, forChainId); + /// @inheritdoc IManagerBase + function getChainsEnabledForReceiving() + external + pure + override(ManagerBase, IManagerBase) + returns (uint16[] memory) + { + return _getEnabledChainsStorage(RECV_ENABLED_CHAINS_SLOT); } - function _enableTransceiverForChain( - address transceiver, + /// @inheritdoc IManagerBase + function getThresholdForChain( + uint16 forChainId + ) public view override(ManagerBase, IManagerBase) returns (uint8) { + return _getThresholdStoragePerChain()[forChainId].num; + } + + // =============== Public Setters ======================================================== + + /// @inheritdoc IManagerBase + function setSendTransceiverBitmapForChain( uint16 forChainId, - bytes32 tag - ) private { - if (transceiver == address(0)) { - revert InvalidTransceiverZeroAddress(); + uint64 indexBitmap + ) public override(ManagerBase, IManagerBase) onlyOwner { + _validateTransceivers(indexBitmap); + mapping(uint16 => _EnabledTransceiverBitmap) storage _bitmaps = + _getPerChainTransceiverBitmapStorage(SEND_TRANSCEIVER_BITMAP_SLOT); + uint64 oldBitmap = _bitmaps[forChainId].bitmap; + _bitmaps[forChainId].bitmap = indexBitmap; + + if (indexBitmap == 0) { + _removeChain(SEND_ENABLED_CHAINS_SLOT, forChainId); + } else { + _addChainIfNeeded(SEND_ENABLED_CHAINS_SLOT, forChainId); } - TransceiverInfo storage transceiverInfo = _getTransceiverInfosStorage()[transceiver]; + emit SendTransceiversUpdatedForChain(forChainId, oldBitmap, indexBitmap); + } + + /// @inheritdoc IManagerBase + function setRecvTransceiverBitmapForChain( + uint16 forChainId, + uint64 indexBitmap, + uint8 threshold + ) public override(ManagerBase, IManagerBase) onlyOwner { + _validateTransceivers(indexBitmap); + + // Validate the threshold against the bitmap. + uint8 numEnabled = countSetBits(indexBitmap); + if (threshold > numEnabled) { + revert ThresholdTooHigh(threshold, numEnabled); + } - if (!transceiverInfo.registered) { - revert NonRegisteredTransceiver(transceiver); + if ((numEnabled != 0) && (threshold == 0)) { + revert ZeroThreshold(); } + // Update the bitmap. mapping(uint16 => _EnabledTransceiverBitmap) storage _bitmaps = - _getPerChainTransceiverBitmapStorage(tag); - _bitmaps[forChainId].bitmap |= uint64(1 << transceiverInfo.index); + _getPerChainTransceiverBitmapStorage(RECV_TRANSCEIVER_BITMAP_SLOT); + uint64 oldBitmap = _bitmaps[forChainId].bitmap; + _bitmaps[forChainId].bitmap = indexBitmap; + + // Update the thresold. + mapping(uint16 => _Threshold) storage _threshold = _getThresholdStoragePerChain(); + uint8 oldThreshold = _threshold[forChainId].num; + _threshold[forChainId].num = threshold; + + // Update the chain list. + if (indexBitmap == 0) { + _removeChain(RECV_ENABLED_CHAINS_SLOT, forChainId); + } else { + _addChainIfNeeded(RECV_ENABLED_CHAINS_SLOT, forChainId); + } + + emit RecvTransceiversUpdatedForChain( + forChainId, oldBitmap, indexBitmap, oldThreshold, threshold + ); + } + + /// @inheritdoc IManagerBase + function setTransceiversForChains( + SetTransceiversForChainEntry[] memory params + ) external override(ManagerBase, IManagerBase) onlyOwner { + for (uint256 idx = 0; idx < params.length; idx++) { + setSendTransceiverBitmapForChain(params[idx].chainId, params[idx].sendBitmap); + setRecvTransceiverBitmapForChain( + params[idx].chainId, params[idx].recvBitmap, params[idx].recvThreshold + ); + } } + // =============== Internal Interface Overrides =================================================== + function _isSendTransceiverEnabledForChain( address transceiver, uint16 forChainId @@ -82,6 +187,27 @@ contract NttManagerWithPerChainTransceivers is NttManagerNoRateLimiting { return (bitmap & uint64(1 << index)) != 0; } + /// @inheritdoc IManagerBase + function isMessageApproved( + bytes32 digest + ) public view override(ManagerBase, IManagerBase) returns (bool) { + uint16 sourceChainId = _getMessageAttestationsStorage()[digest].sourceChainId; + uint8 threshold = getThresholdForChain(sourceChainId); + return messageAttestations(digest) >= threshold && threshold > 0; + } + + function _getMessageAttestations( + bytes32 digest + ) internal view override returns (uint64) { + AttestationInfo memory attInfo = _getMessageAttestationsStorage()[digest]; + uint64 enabledTransceiverBitmap = _getEnabledTransceiversBitmap(); + uint64 enabledTransceiversForChain = + _getEnabledRecvTransceiversForChain(attInfo.sourceChainId); + return attInfo.attestedTransceivers & enabledTransceiverBitmap & enabledTransceiversForChain; + } + + // ==================== Implementation ========================= + function _getEnabledRecvTransceiversForChain( uint16 forChainId ) internal view returns (uint64 bitmap) { @@ -93,12 +219,6 @@ contract NttManagerWithPerChainTransceivers is NttManagerNoRateLimiting { bytes32 tag ) private view returns (uint64 bitmap) { bitmap = _getPerChainTransceiverBitmapStorage(tag)[forChainId].bitmap; - if (bitmap == 0) { - // Avoid an extra storage slot read and just return all ones. This works - // because the value gets combined with the enabled transceivers on the - // receive side and the specific transceiver index on the send side. - bitmap = type(uint64).max; - } } function _getPerChainTransceiverBitmapStorage( @@ -110,74 +230,99 @@ contract NttManagerWithPerChainTransceivers is NttManagerNoRateLimiting { } } - // ==================== Override / implementation of threshold stuff ========================= + function _getThresholdStoragePerChain() + private + pure + returns (mapping(uint16 => _Threshold) storage $) + { + // Reusing the global storage slot is safe because the mapping doesn't write into the slot itself. + uint256 slot = uint256(THRESHOLD_SLOT); + assembly ("memory-safe") { + $.slot := slot + } + } - /// @inheritdoc IManagerBase - function setPerChainThreshold( - uint16 forChainId, - uint8 threshold - ) external override(ManagerBase, IManagerBase) onlyOwner { - if (threshold == 0) { - revert ZeroThreshold(); + function _validateTransceivers( + uint64 indexBitmap + ) internal view { + uint8 index = 0; + while (indexBitmap != 0) { + if (indexBitmap & 0x01 == 1) { + _validateTransceiver(index); + } + indexBitmap = indexBitmap >> 1; + index++; } + } - // They can't set the threshold greater than the number of enabled transceivers. - uint64 enabledTransceiverBitmap = _getEnabledTransceiversBitmap(); - uint64 enabledTransceiversForChain = _getEnabledRecvTransceiversForChain(forChainId); - uint8 numEnabledTransceivers = - countSetBits(enabledTransceiverBitmap & enabledTransceiversForChain); - if (threshold > numEnabledTransceivers) { - revert ThresholdTooHigh(threshold, numEnabledTransceivers); + function _validateTransceiver( + uint8 index + ) internal view { + address[] storage _enabledTransceivers = _getEnabledTransceiversStorage(); + if (index >= _enabledTransceivers.length) { + revert TransceiverIndexTooLarge(index, _enabledTransceivers.length); } - mapping(uint16 => _Threshold) storage _threshold = _getThresholdStoragePerChain(); - uint8 oldThreshold = _threshold[forChainId].num; + address transceiverAddr = _enabledTransceivers[index]; + mapping(address => TransceiverInfo) storage transceiverInfos = _getTransceiverInfosStorage(); - _threshold[forChainId].num = threshold; - _checkThresholdInvariants(_threshold[forChainId].num); + if (transceiverInfos[transceiverAddr].index != index) { + revert InvalidTransceiverIndex(index, transceiverInfos[transceiverAddr].index); + } - emit PerChainThresholdChanged(forChainId, oldThreshold, threshold); - } + if (!transceiverInfos[transceiverAddr].registered) { + revert TransceiverNotRegistered(index, transceiverAddr); + } - function getPerChainThreshold( - uint16 forChainId - ) public view override(ManagerBase, IManagerBase) returns (uint8) { - uint8 threshold = _getThresholdStoragePerChain()[forChainId].num; - if (threshold == 0) { - return _getThresholdStorage().num; + if (!transceiverInfos[transceiverAddr].enabled) { + revert TransceiverNotEnabled(index, transceiverAddr); } - return threshold; } - function _getThresholdStoragePerChain() - private - pure - returns (mapping(uint16 => _Threshold) storage $) - { - // TODO: this is safe (reusing the storage slot, because the mapping - // doesn't write into the slot itself) buy maybe we shouldn't? - uint256 slot = uint256(THRESHOLD_SLOT); - assembly ("memory-safe") { - $.slot := slot + function _removeChain(bytes32 tag, uint16 forChainId) private { + uint16[] storage chains = _getEnabledChainsStorage(tag); + uint256 len = chains.length; + for (uint256 idx = 0; (idx < len); idx++) { + if (chains[idx] == forChainId) { + if (len > 1) { + chains[idx] = chains[len - 1]; + } + chains.pop(); + return; + } } } - /// @inheritdoc IManagerBase - function isMessageApproved( - bytes32 digest - ) public view override(ManagerBase, IManagerBase) returns (bool) { - uint16 sourceChainId = _getMessageAttestationsStorage()[digest].sourceChainId; - uint8 threshold = getPerChainThreshold(sourceChainId); - return messageAttestations(digest) >= threshold && threshold > 0; + function _addChainIfNeeded(bytes32 tag, uint16 forChainId) private { + uint16[] storage chains = _getEnabledChainsStorage(tag); + uint256 zeroIdx = type(uint256).max; + uint256 len = chains.length; + for (uint256 idx = 0; (idx < len); idx++) { + if (chains[idx] == forChainId) { + return; + } + if (chains[idx] == 0) { + zeroIdx = idx; + } + } + + if (zeroIdx != type(uint256).max) { + chains[zeroIdx] = forChainId; + } else { + chains.push(forChainId); + } } - function _getMessageAttestations( - bytes32 digest - ) internal view override returns (uint64) { - AttestationInfo memory attInfo = _getMessageAttestationsStorage()[digest]; - uint64 enabledTransceiverBitmap = _getEnabledTransceiversBitmap(); - uint64 enabledTransceiversForChain = - _getEnabledRecvTransceiversForChain(attInfo.sourceChainId); - return attInfo.attestedTransceivers & enabledTransceiverBitmap & enabledTransceiversForChain; + // function _copyEnabledChains(bytes32 tag) uint16[] memory { + // uint16[] ret = new + // } + + function _getEnabledChainsStorage( + bytes32 tag + ) internal pure returns (uint16[] storage $) { + uint256 slot = uint256(tag); + assembly ("memory-safe") { + $.slot := slot + } } } diff --git a/evm/src/interfaces/IManagerBase.sol b/evm/src/interfaces/IManagerBase.sol index bfd08a184..92562dcb7 100644 --- a/evm/src/interfaces/IManagerBase.sol +++ b/evm/src/interfaces/IManagerBase.sol @@ -34,6 +34,14 @@ interface IManagerBase { uint8 num; } + /// @notice The structure of a per-chain entry in the call to setTransceiversForChains. + struct SetTransceiversForChainEntry { + uint64 sendBitmap; + uint64 recvBitmap; + uint16 chainId; + uint8 recvThreshold; + } + /// @notice Emitted when a message has been attested to. /// @dev Topic0 /// 0x35a2101eaac94b493e0dfca061f9a7f087913fde8678e7cde0aca9897edba0e5. @@ -49,14 +57,6 @@ interface IManagerBase { /// @param threshold The new threshold. event ThresholdChanged(uint8 oldThreshold, uint8 threshold); - /// @notice Emmitted when the per-chain threshold required transceivers is changed. - /// @dev Topic0 - /// 0x899b344e9e176bdedb9f57f13e014ff93a874404737f35d71dd95a2e858814d8. - /// @param chainId The chain to which the threshold applies. - /// @param oldThreshold The old threshold. - /// @param threshold The new threshold. - event PerChainThresholdChanged(uint16 chainId, uint8 oldThreshold, uint8 threshold); - /// @notice Emitted when an transceiver is removed from the nttManager. /// @dev Topic0 /// 0xf05962b5774c658e85ed80c91a75af9d66d2af2253dda480f90bce78aff5eda5. @@ -65,20 +65,6 @@ interface IManagerBase { /// @param threshold The current threshold of transceivers. event TransceiverAdded(address transceiver, uint256 transceiversNum, uint8 threshold); - /// @notice Emitted when a transceiver is enabled for sending on a chain. - /// @dev Topic0 - /// 0x8b14d833f2eae4d6fc1d6037cc37fc69ed72e184b586ab5cda3dda94cc4cc37d. - /// @param transceiver The address of the transceiver. - /// @param chainId The chain to which the send applies. - event SendTransceiverEnabledForChain(address transceiver, uint16 chainId); - - /// @notice Emitted when a transceiver is enabled for receiving on a chain. - /// @dev Topic0 - /// 0xf05962b5774c658e85ed80c91a75af9d66d2af2253dda480f90bce78aff5eda5. - /// @param transceiver The address of the transceiver. - /// @param chainId The chain to which the receive applies. - event RecvTransceiverEnabledForChain(address transceiver, uint16 chainId); - /// @notice Emitted when an transceiver is removed from the nttManager. /// @dev Topic0 /// 0x697a3853515b88013ad432f29f53d406debc9509ed6d9313dcfe115250fcd18f. @@ -86,6 +72,26 @@ interface IManagerBase { /// @param threshold The current threshold of transceivers. event TransceiverRemoved(address transceiver, uint8 threshold); + /// @notice Emitted when the sending transceivers are updated for a chain. + /// @dev Topic0 + /// 0xe3bed59083cdad1d552b8eef7d3acc80adb78da6c6f375ae3adf5cb4823b2619 + /// @param chainId The chain that was updated. + /// @param oldBitmap The original index bitmap. + /// @param oldBitmap The updated index bitmap. + event SendTransceiversUpdatedForChain(uint16 chainId, uint64 oldBitmap, uint64 newBitmap); + + /// @notice Emitted when the receivinging transceivers are updated for a chain. + /// @dev Topic0 + /// 0xd09fdac2bd3e794a578992bfe77134765623d22a2b3201e2994f681828160f2f + /// @param chainId The chain that was updated. + /// @param oldBitmap The original index bitmap. + /// @param oldBitmap The updated index bitmap. + /// @param oldThreshold The original receive threshold. + /// @param newThreshold The updated receive threshold. + event RecvTransceiversUpdatedForChain( + uint16 chainId, uint64 oldBitmap, uint64 newBitmap, uint8 oldThreshold, uint8 newThreshold + ); + /// @notice payment for a transfer is too low. /// @param requiredPayment The required payment. /// @param providedPayment The provided payment. @@ -156,13 +162,6 @@ interface IManagerBase { uint8 threshold ) external; - /// @notice Sets the per-chain threshold for the number of attestations required for a message - /// to be considered valid. Note that if a threshold is not specified for a chain, the default applies. - /// @param chainId The chain for which the threshold applies. - /// @param threshold The new threshold. - /// @dev This method can only be executed by the `owner`. - function setPerChainThreshold(uint16 chainId, uint8 threshold) external; - /// @notice Sets the transceiver for the given chain. /// @param transceiver The address of the transceiver. /// @dev This method can only be executed by the `owner`. @@ -177,18 +176,6 @@ interface IManagerBase { address transceiver ) external; - /// @notice Enables the transceiver for sending on the given chain. - /// @param transceiver The address of the transceiver. - /// @param chainId The chain for which the threshold applies. - /// @dev This method can only be executed by the `owner`. - function enableSendTransceiverForChain(address transceiver, uint16 chainId) external; - - /// @notice Enables the transceiver for receiving on the given chain. - /// @param transceiver The address of the transceiver. - /// @param chainId The chain for which the threshold applies. - /// @dev This method can only be executed by the `owner`. - function enableRecvTransceiverForChain(address transceiver, uint16 chainId) external; - /// @notice Checks if a message has been approved. The message should have at least /// the minimum threshold of attestations from distinct endpoints. /// @param digest The digest of the message. @@ -226,13 +213,6 @@ interface IManagerBase { /// it to be considered valid and acted upon. function getThreshold() external view returns (uint8); - /// @notice Returns the number of Transceivers that must attest to a msgId for - /// it to be considered valid and acted upon. - /// @param chainId The chain for which the threshold applies. - function getPerChainThreshold( - uint16 chainId - ) external view returns (uint8); - /// @notice Returns a boolean indicating if the transceiver has attested to the message. /// @param digest The digest of the message. /// @param index The index of the transceiver @@ -254,4 +234,50 @@ interface IManagerBase { /// @notice Returns the chain ID. function chainId() external view returns (uint16); + + /// @notice Returns the bitmap of send transceivers enabled for a chain. + /// @param forChainId The chain for which sending is enabled. + function getSendTransceiverBitmapForChain( + uint16 forChainId + ) external view returns (uint64); + + /// @notice Returns the bitmap of receive transceivers enabled for a chain. + /// @param forChainId The chain for which receiving is enabled. + function getRecvTransceiverBitmapForChain( + uint16 forChainId + ) external view returns (uint64); + + /// @notice Returns the set of chains for which sending is enabled. + function getChainsEnabledForSending() external view returns (uint16[] memory); + + /// @notice Returns the set of chains for which receiving is enabled. + function getChainsEnabledForReceiving() external view returns (uint16[] memory); + + /// @notice Sets the bitmap of transceivers enabled for sending for a chain. + /// @param forChainId The chain to be updated. + /// @param indexBitmap The bitmap of transceiver indexes that are enabled. + function setSendTransceiverBitmapForChain(uint16 forChainId, uint64 indexBitmap) external; + + /// @notice Sets the bitmap of transceivers enabled for receiving for a chain. + /// @param forChainId The chain to be updated. + /// @param indexBitmap The bitmap of transceiver indexes that are enabled. + /// @param threshold The receive threshold for the chain. + function setRecvTransceiverBitmapForChain( + uint16 forChainId, + uint64 indexBitmap, + uint8 threshold + ) external; + + /// @notice Sets the transceiver bitmaps and thresholds for a set of chains. + /// @param params The values to be applied for a set of chains. + function setTransceiversForChains( + SetTransceiversForChainEntry[] memory params + ) external; + + /// @notice Returns the number of Transceivers that must attest to a msgId for + /// it to be considered valid and acted upon. + /// @param forChainId The chain for which the threshold applies. + function getThresholdForChain( + uint16 forChainId + ) external view returns (uint8); } diff --git a/evm/test/PerChainTransceivers.t.sol b/evm/test/PerChainTransceivers.t.sol index 646c028f9..a91376f83 100755 --- a/evm/test/PerChainTransceivers.t.sol +++ b/evm/test/PerChainTransceivers.t.sol @@ -269,100 +269,276 @@ contract TestPerChainTransceivers is Test, IRateLimiterEvents { secondWormholeTransceiverChain3.setWormholePeer( chainId2, bytes32(uint256(uint160(address(secondWormholeTransceiverChain2)))) ); + } + + function test_transceiverSetters() public { + // Make sure nothing is enabled for either sending or receiving. + require( + nttManagerChain1.getChainsEnabledForSending().length == 0, + "There should be no chains enabled for sending to start with" + ); + require( + nttManagerChain1.getChainsEnabledForReceiving().length == 0, + "There should be no chains enabled for receiving to start with" + ); - // Set the thresholds. - require(nttManagerChain1.getThreshold() != 0, "Threshold is zero with active transceivers"); + // Chain 2 + require( + nttManagerChain1.getSendTransceiverBitmapForChain(chainId2) == 0, + "There should be nothing enabled for sending on chain two to start with" + ); + require( + nttManagerChain1.getRecvTransceiverBitmapForChain(chainId2) == 0, + "There should be nothing enabled for receiving on chain two to start with" + ); + + // Chain 3 + require( + nttManagerChain1.getSendTransceiverBitmapForChain(chainId3) == 0, + "There should be nothing enabled for sending on chain three to start with" + ); + require( + nttManagerChain1.getRecvTransceiverBitmapForChain(chainId3) == 0, + "There should be nothing enabled for receiving on chain three to start with" + ); - // Actually set it - nttManagerChain1.setThreshold(1); - nttManagerChain2.setThreshold(1); - nttManagerChain3.setThreshold(1); + // Enable a sender on chain two. + nttManagerChain1.setSendTransceiverBitmapForChain(chainId2, 0x02); + require( + nttManagerChain1.getSendTransceiverBitmapForChain(chainId2) == 0x02, + "Sending bitmap is wrong for chain two #1" + ); + uint16[] memory sendChains = nttManagerChain1.getChainsEnabledForSending(); + require(sendChains.length == 1, "There should be one chain enabled for sending"); + require(sendChains[0] == chainId2, "Chain two should be enabled for sending"); - // On chain 1, set the threshold to chain 3 to be different. - nttManagerChain1.setPerChainThreshold(chainId3, 2); - nttManagerChain3.setPerChainThreshold(chainId1, 2); + // Enable a receiver on chain two. + nttManagerChain1.setRecvTransceiverBitmapForChain(chainId2, 0x01, 1); + require( + nttManagerChain1.getRecvTransceiverBitmapForChain(chainId2) == 0x01, + "Receiving bitmap is wrong for chain two #1" + ); + uint16[] memory recvChains = nttManagerChain1.getChainsEnabledForReceiving(); + require(recvChains.length == 1, "There should be one chain enabled for receiving"); + require(recvChains[0] == chainId2, "Chain two should be enabled for receiving #1"); - require(nttManagerChain1.getThreshold() == 1, "Default threshold is wrong"); + // Enable a sender on chain three. + nttManagerChain1.setSendTransceiverBitmapForChain(chainId3, 0x01); require( - nttManagerChain1.getPerChainThreshold(chainId3) == 2, "Threshold for chain 3 is wrong" + nttManagerChain1.getSendTransceiverBitmapForChain(chainId3) == 0x01, + "Sending bitmap is wrong for chain three #1" ); + sendChains = nttManagerChain1.getChainsEnabledForSending(); + require(sendChains.length == 2, "There should be one chain enabled for sending"); + require(sendChains[0] == chainId2, "Chain two should be enabled for sending"); + require(sendChains[1] == chainId3, "Chain three should be enabled for sending"); - // Since we haven't set the per-chain threshold for chain 2, it should return the default + // Enable a receiver on chain three. + nttManagerChain1.setRecvTransceiverBitmapForChain(chainId3, 0x02, 1); require( - nttManagerChain1.getPerChainThreshold(chainId2) == 1, "Threshold for chain 2 is wrong" + nttManagerChain1.getRecvTransceiverBitmapForChain(chainId3) == 0x02, + "Receiving bitmap is wrong for chain three #1" + ); + recvChains = nttManagerChain1.getChainsEnabledForReceiving(); + require(recvChains.length == 2, "There should be two chains enabled for receiving"); + require(recvChains[0] == chainId2, "Chain two should be enabled for receiving #2"); + require(recvChains[1] == chainId3, "Chain three should be enabled for receiving #1"); + require( + nttManagerChain1.getThresholdForChain(chainId3) == 1, + "Threshold is wrong for chain three #1" ); - } - function test_transceiverSetters() public { - // If we haven't enabled per-chain send transceivers for this chain, the getter should return true. + // Enable two receivers on chain two. + nttManagerChain1.setRecvTransceiverBitmapForChain(chainId2, 0x03, 2); + require( + nttManagerChain1.getRecvTransceiverBitmapForChain(chainId2) == 0x03, + "Receiving bitmap is wrong for chain three #2" + ); + recvChains = nttManagerChain1.getChainsEnabledForReceiving(); + require(recvChains.length == 2, "There should be two chains enabled for receiving"); + require(recvChains[0] == chainId2, "Chain two should be enabled for receiving #3"); + require(recvChains[1] == chainId3, "Chain three should be enabled for receiving #2"); require( - nttManagerChain1.isSendTransceiverEnabledForChain(address(nttManagerChain3), chainId3), - "Send transceiver should be enabled by default" + nttManagerChain1.getThresholdForChain(chainId2) == 2, + "Threshold is wrong for chain three #2" ); - // If we haven't enabled per-chain receive transceivers, the getter should return everything is enabled. + // Disable one receiver on chain two. + nttManagerChain1.setRecvTransceiverBitmapForChain(chainId2, 0x02, 1); require( - nttManagerChain1.getEnabledRecvTransceiversForChain(chainId3) == type(uint64).max, - "Receive transceiver should be enabled by default" + nttManagerChain1.getRecvTransceiverBitmapForChain(chainId2) == 0x02, + "Receiving bitmap is wrong for chain two #3" + ); + recvChains = nttManagerChain1.getChainsEnabledForReceiving(); + require(recvChains.length == 2, "There should be two chains enabled for receiving"); + require(recvChains[0] == chainId2, "Chain two should be enabled for receiving #4"); + require(recvChains[1] == chainId3, "Chain three should be enabled for receiving #3"); + require( + nttManagerChain1.getThresholdForChain(chainId2) == 1, + "Threshold is wrong for chain two #3" ); - nttManagerChain1.enableSendTransceiverForChain(address(wormholeTransceiverChain1), chainId2); - nttManagerChain1.enableRecvTransceiverForChain( - address(secondWormholeTransceiverChain1), chainId3 + // Disable the other receiver on chain two. + nttManagerChain1.setRecvTransceiverBitmapForChain(chainId2, 0x00, 0); + require( + nttManagerChain1.getRecvTransceiverBitmapForChain(chainId2) == 0x00, + "Receiving bitmap is wrong for chain two #4" + ); + recvChains = nttManagerChain1.getChainsEnabledForReceiving(); + require(recvChains.length == 1, "There should be only one chain enabled for receiving"); + require(recvChains[0] == chainId3, "Chain three should be enabled for receiving #5"); + require( + nttManagerChain1.getThresholdForChain(chainId2) == 0, + "Threshold is wrong for chain two #4" ); - // Once we enable a transceiver for a chain, that is the only one enabled for that chain. + // Disable one receiver on chain three. + nttManagerChain1.setRecvTransceiverBitmapForChain(chainId3, 0x02, 1); require( - nttManagerChain1.isSendTransceiverEnabledForChain( - address(wormholeTransceiverChain1), chainId2 - ), - "First transceiver should be enabled for sending on chain 2" + nttManagerChain1.getRecvTransceiverBitmapForChain(chainId3) == 0x02, + "Receiving bitmap is wrong for chain three #3" ); + recvChains = nttManagerChain1.getChainsEnabledForReceiving(); + require(recvChains.length == 1, "There should be one chain enabled for receiving"); + require(recvChains[0] == chainId3, "Chain three should be enabled for receiving #4"); require( - !nttManagerChain1.isSendTransceiverEnabledForChain( - address(secondWormholeTransceiverChain1), chainId2 - ), - "Second transceiver should not be enabled for sending on chain 2" + nttManagerChain1.getThresholdForChain(chainId3) == 1, + "Threshold is wrong for chain three #3" ); + + // Disable the other receiver on chain three. + nttManagerChain1.setRecvTransceiverBitmapForChain(chainId3, 0x00, 0); require( - nttManagerChain1.getEnabledRecvTransceiversForChain(chainId3) == 0x2, - "Only second transceiver should be enabled for receiving on chain 3" + nttManagerChain1.getRecvTransceiverBitmapForChain(chainId3) == 0x00, + "Receiving bitmap is wrong for chain three #5" + ); + recvChains = nttManagerChain1.getChainsEnabledForReceiving(); + require(recvChains.length == 0, "There should be no chains enabled for receiving"); + require( + nttManagerChain1.getThresholdForChain(chainId3) == 0, + "Threshold is wrong for chain three #5" ); - // And for chains we didn't touch, the defaults should still be in place. + // Make sure our senders haven't changed. + nttManagerChain1.setSendTransceiverBitmapForChain(chainId2, 0x02); require( - nttManagerChain1.isSendTransceiverEnabledForChain(address(nttManagerChain3), chainId3), - "Send transceiver should be enabled by default for untouched chain" + nttManagerChain1.getSendTransceiverBitmapForChain(chainId2) == 0x02, + "Sending bitmap is wrong for chain two #2" ); require( - nttManagerChain1.getEnabledRecvTransceiversForChain(chainId2) == type(uint64).max, - "Receive transceiver should be enabled by default for untouched chain" + nttManagerChain1.getSendTransceiverBitmapForChain(chainId3) == 0x01, + "Sending bitmap is wrong for chain three #2" ); + sendChains = nttManagerChain1.getChainsEnabledForSending(); + require(sendChains.length == 2, "There should be one chain enabled for sending"); + require(sendChains[0] == chainId2, "Chain two should be enabled for sending"); + require(sendChains[1] == chainId3, "Chain three should be enabled for sending"); } - function test_someReverts() public { - // Can't enable transceiver with address zero. - vm.expectRevert( - abi.encodeWithSelector(TransceiverRegistry.InvalidTransceiverZeroAddress.selector) + function test_setTransceiversForChains() public { + IManagerBase.SetTransceiversForChainEntry[] memory params = + new IManagerBase.SetTransceiversForChainEntry[](2); + + params[0] = IManagerBase.SetTransceiversForChainEntry({ + chainId: chainId2, + sendBitmap: 0x02, + recvBitmap: 0x01, + recvThreshold: 1 + }); + + params[1] = IManagerBase.SetTransceiversForChainEntry({ + chainId: chainId3, + sendBitmap: 0x02, + recvBitmap: 0x03, + recvThreshold: 2 + }); + + nttManagerChain1.setTransceiversForChains(params); + + // Validate chain two. + require( + nttManagerChain1.getSendTransceiverBitmapForChain(chainId2) == 0x02, + "Sending bitmap is wrong for chain two" + ); + require( + nttManagerChain1.getRecvTransceiverBitmapForChain(chainId2) == 0x01, + "Receiving bitmap is wrong for chain two" + ); + require( + nttManagerChain1.getThresholdForChain(chainId2) == 1, "Threshold is wrong for chain two" + ); + + // Validate chain three. + require( + nttManagerChain1.getSendTransceiverBitmapForChain(chainId3) == 0x02, + "Sending bitmap is wrong for chain three" + ); + require( + nttManagerChain1.getRecvTransceiverBitmapForChain(chainId3) == 0x03, + "Receiving bitmap is wrong for chain three" + ); + require( + nttManagerChain1.getThresholdForChain(chainId3) == 2, + "Threshold is wrong for chain three" ); - nttManagerChain1.enableSendTransceiverForChain(address(0), chainId2); - // Can't enable transceiver for an unknown address. + // Validate the chain lists. + uint16[] memory sendChains = nttManagerChain1.getChainsEnabledForSending(); + require(sendChains.length == 2, "There should be two chains enabled for sending"); + require(sendChains[0] == chainId2, "Chain two should be enabled for sending #3"); + require(sendChains[1] == chainId3, "Chain three should be enabled for sending #2"); + + uint16[] memory recvChains = nttManagerChain1.getChainsEnabledForReceiving(); + require(recvChains.length == 2, "There should be two chains enabled for receiving"); + require(recvChains[0] == chainId2, "Chain two should be enabled for receiving #3"); + require(recvChains[1] == chainId3, "Chain three should be enabled for receiving #2"); + } + + function test_someReverts() public { + vm.expectRevert(abi.encodeWithSelector(IManagerBase.ThresholdTooHigh.selector, 2, 1)); + nttManagerChain1.setRecvTransceiverBitmapForChain(chainId2, 0x01, 2); + + vm.expectRevert(abi.encodeWithSelector(IManagerBase.ZeroThreshold.selector)); + nttManagerChain1.setRecvTransceiverBitmapForChain(chainId2, 0x01, 0); + vm.expectRevert( abi.encodeWithSelector( - TransceiverRegistry.NonRegisteredTransceiver.selector, address(this) + NttManagerWithPerChainTransceivers.TransceiverIndexTooLarge.selector, 6, 2 ) ); - nttManagerChain1.enableSendTransceiverForChain(address(this), chainId2); + nttManagerChain1.setRecvTransceiverBitmapForChain(chainId2, 0x40, 0); - // Can set the per-chain threshold to zero. - vm.expectRevert(abi.encodeWithSelector(IManagerBase.ZeroThreshold.selector)); - nttManagerChain1.setPerChainThreshold(chainId2, 0); + nttManagerChain1.setRecvTransceiverBitmapForChain(chainId2, 0, 0); + nttManagerChain1.removeTransceiver(address(wormholeTransceiverChain1)); } // This test does a transfer between chain one and chain two. - // Since chain two uses the default threshold, posting a VAA from only one transceiver completes the transfer. - function test_defaultThreshold() public { + // Since the receive thresholds are set to one, posting a VAA from only one transceiver completes the transfer. + function test_thresholdLessThanNumReceivers() public { + IManagerBase.SetTransceiversForChainEntry[] memory nttManager1Params = + new IManagerBase.SetTransceiversForChainEntry[](1); + + nttManager1Params[0] = IManagerBase.SetTransceiversForChainEntry({ + chainId: chainId2, + sendBitmap: 0x03, + recvBitmap: 0x03, + recvThreshold: 1 + }); + + nttManagerChain1.setTransceiversForChains(nttManager1Params); + + IManagerBase.SetTransceiversForChainEntry[] memory nttManager2Params = + new IManagerBase.SetTransceiversForChainEntry[](1); + + nttManager2Params[0] = IManagerBase.SetTransceiversForChainEntry({ + chainId: chainId1, + sendBitmap: 0x03, + recvBitmap: 0x03, + recvThreshold: 1 + }); + + nttManagerChain2.setTransceiversForChains(nttManager2Params); + vm.chainId(chainId1); // Setting up the transfer @@ -412,7 +588,7 @@ contract TestPerChainTransceivers is Test, IRateLimiterEvents { wormholeTransceiverChain2.receiveMessage(encodedVMs[0]); uint256 supplyAfter = token2.totalSupply(); - require(sendingAmount + supplyBefore == supplyAfter, "Supplies dont match"); + require(sendingAmount + supplyBefore == supplyAfter, "Supplies dont match #1"); require(token2.balanceOf(userB) == sendingAmount, "User didn't receive tokens"); require(token2.balanceOf(address(nttManagerChain2)) == 0, "NttManager has unintended funds"); @@ -481,140 +657,30 @@ contract TestPerChainTransceivers is Test, IRateLimiterEvents { // This test does a transfer between chain one and chain three. // Since the threshold for these two chains is two, the transfer is not completed until both VAAs are posted. - function test_perChainThreshold() public { - vm.chainId(chainId1); - - // Setting up the transfer - DummyToken token1 = DummyToken(nttManagerChain1.token()); - DummyToken token3 = DummyTokenMintAndBurn(nttManagerChain3.token()); - - uint8 decimals = token1.decimals(); - uint256 sendingAmount = 5 * 10 ** decimals; - token1.mintDummy(address(userA), 5 * 10 ** decimals); - vm.startPrank(userA); - token1.approve(address(nttManagerChain1), sendingAmount); - - vm.recordLogs(); - - // Send token from chain 1 to chain 3, userB. - { - uint256 nttManagerBalanceBefore = token1.balanceOf(address(nttManagerChain1)); - uint256 userBalanceBefore = token1.balanceOf(address(userA)); - nttManagerChain1.transfer(sendingAmount, chainId3, bytes32(uint256(uint160(userB)))); - - // Balance check on funds going in and out working as expected - uint256 nttManagerBalanceAfter = token1.balanceOf(address(nttManagerChain1)); - uint256 userBalanceAfter = token1.balanceOf(address(userB)); - require( - nttManagerBalanceBefore + sendingAmount == nttManagerBalanceAfter, - "Should be locking the tokens" - ); - require( - userBalanceBefore - sendingAmount == userBalanceAfter, - "User should have sent tokens" - ); - } - - vm.stopPrank(); - - // Get and sign the log to go down the other pipes. There should be two messages since we have two transceivers. - Vm.Log[] memory entries = guardian.fetchWormholeMessageFromLog(vm.getRecordedLogs()); - require(2 == entries.length, "Unexpected number of log entries 3"); - bytes[] memory encodedVMs = new bytes[](entries.length); - for (uint256 i = 0; i < encodedVMs.length; i++) { - encodedVMs[i] = guardian.fetchSignedMessageFromLogs(entries[i], chainId1); - } - - // Chain3 verification and checks - vm.chainId(chainId3); - - uint256 supplyBefore = token3.totalSupply(); - - // Submit the first message on chain 3. The numbers shouldn't change yet since the threshold is two. - wormholeTransceiverChain3.receiveMessage(encodedVMs[0]); - uint256 supplyAfter = token3.totalSupply(); - - require(supplyBefore == supplyAfter, "Supplies changed early"); - require(token3.balanceOf(userB) == 0, "User receive tokens early"); - require(token3.balanceOf(address(nttManagerChain3)) == 0, "NttManager has unintended funds"); - - // Submit the second message and the transfer should complete. - secondWormholeTransceiverChain3.receiveMessage(encodedVMs[1]); - supplyAfter = token3.totalSupply(); - - require(sendingAmount + supplyBefore == supplyAfter, "Supplies dont match"); - require(token3.balanceOf(userB) == sendingAmount, "User didn't receive tokens"); - require(token3.balanceOf(address(nttManagerChain3)) == 0, "NttManager has unintended funds"); - - // Go back the other way from a THIRD user - vm.prank(userB); - token3.transfer(userC, sendingAmount); - - vm.startPrank(userC); - token3.approve(address(nttManagerChain3), sendingAmount); - vm.recordLogs(); - - // Supply checks on the transfer - supplyBefore = token3.totalSupply(); - nttManagerChain3.transfer( - sendingAmount, - chainId1, - toWormholeFormat(userD), - toWormholeFormat(userC), - false, - encodeTransceiverInstruction(true) - ); - - supplyAfter = token3.totalSupply(); - - require(sendingAmount - supplyBefore == supplyAfter, "Supplies don't match"); - require(token3.balanceOf(userB) == 0, "OG user receive tokens"); - require(token3.balanceOf(userC) == 0, "Sending user didn't receive tokens"); - require( - token3.balanceOf(address(nttManagerChain3)) == 0, - "NttManager didn't receive unintended funds" - ); - - // Get and sign the log to go down the other pipe. Thank you to whoever wrote this code in the past! - entries = guardian.fetchWormholeMessageFromLog(vm.getRecordedLogs()); - require(2 == entries.length, "Unexpected number of log entries for response"); - encodedVMs = new bytes[](entries.length); - for (uint256 i = 0; i < encodedVMs.length; i++) { - encodedVMs[i] = guardian.fetchSignedMessageFromLogs(entries[i], chainId3); - } - - // Chain1 verification and checks with the receiving of the message - vm.chainId(chainId1); + function test_thresholdEqualToNumberOfReceivers() public { + IManagerBase.SetTransceiversForChainEntry[] memory nttManager1Params = + new IManagerBase.SetTransceiversForChainEntry[](1); - // Submit the first message back on chain one. Nothing should happen because our threshold is two. - supplyBefore = token1.totalSupply(); - wormholeTransceiverChain1.receiveMessage(encodedVMs[0]); - supplyAfter = token1.totalSupply(); + nttManager1Params[0] = IManagerBase.SetTransceiversForChainEntry({ + chainId: chainId3, + sendBitmap: 0x03, + recvBitmap: 0x03, + recvThreshold: 2 + }); - require(supplyBefore == supplyAfter, "Supplies don't match between operations"); - require(token1.balanceOf(userB) == 0, "OG user receive tokens"); - require(token1.balanceOf(userC) == 0, "Sending user didn't receive tokens"); - require(token1.balanceOf(userD) == 0, "User received funds before they should"); + nttManagerChain1.setTransceiversForChains(nttManager1Params); - // Submit the second message back on chain one. This should update the balance. - supplyBefore = token1.totalSupply(); - secondWormholeTransceiverChain1.receiveMessage(encodedVMs[1]); - supplyAfter = token1.totalSupply(); + IManagerBase.SetTransceiversForChainEntry[] memory nttManager3Params = + new IManagerBase.SetTransceiversForChainEntry[](1); - require(supplyBefore == supplyAfter, "Supplies don't match between operations"); - require(token1.balanceOf(userB) == 0, "OG user receive tokens"); - require(token1.balanceOf(userC) == 0, "Sending user didn't receive tokens"); - require(token1.balanceOf(userD) == sendingAmount, "User received funds"); - } + nttManager3Params[0] = IManagerBase.SetTransceiversForChainEntry({ + chainId: chainId1, + sendBitmap: 0x03, + recvBitmap: 0x03, + recvThreshold: 2 + }); - // This test does a transfer between chain one and chain three. - // It sets the threshold for chain three on chain one to one, so the transfer should complete when the first VAA is posted. - function test_thresholdLessThanNumReceivers() public { - nttManagerChain1.setPerChainThreshold(chainId3, 1); - require( - nttManagerChain1.getPerChainThreshold(chainId3) == 1, - "Failed to set per-chain threshold" - ); + nttManagerChain3.setTransceiversForChains(nttManager3Params); vm.chainId(chainId1); @@ -720,32 +786,25 @@ contract TestPerChainTransceivers is Test, IRateLimiterEvents { // Chain1 verification and checks with the receiving of the message vm.chainId(chainId1); - // Submit the first message back on chain one. Since the threshold from chain three is one, the numbers should update. - // Just for fun, we are having the second transceiver receive the message first. The order doesn't matter. + // Submit the first message back on chain one. Nothing should happen because our threshold is two. supplyBefore = token1.totalSupply(); - secondWormholeTransceiverChain1.receiveMessage(encodedVMs[1]); + wormholeTransceiverChain1.receiveMessage(encodedVMs[0]); supplyAfter = token1.totalSupply(); require(supplyBefore == supplyAfter, "Supplies don't match between operations"); require(token1.balanceOf(userB) == 0, "OG user receive tokens"); require(token1.balanceOf(userC) == 0, "Sending user didn't receive tokens"); - require( - token1.balanceOf(userD) == sendingAmount, - "Transfer did not complete after first message" - ); + require(token1.balanceOf(userD) == 0, "User received funds before they should"); - // Submitting the second message back on chain one should not change anything. + // Submit the second message back on chain one. This should update the balance. supplyBefore = token1.totalSupply(); - wormholeTransceiverChain1.receiveMessage(encodedVMs[0]); + secondWormholeTransceiverChain1.receiveMessage(encodedVMs[1]); supplyAfter = token1.totalSupply(); require(supplyBefore == supplyAfter, "Supplies don't match between operations"); require(token1.balanceOf(userB) == 0, "OG user receive tokens"); require(token1.balanceOf(userC) == 0, "Sending user didn't receive tokens"); - require( - token1.balanceOf(userD) == sendingAmount, - "Second message updated the balance when it shouldn't have" - ); + require(token1.balanceOf(userD) == sendingAmount, "User received funds"); } function encodeTransceiverInstruction( diff --git a/evm/test/PerChainTransceiversDemo.t.sol b/evm/test/PerChainTransceiversDemo.t.sol index 9c0f89227..a63b3e48a 100755 --- a/evm/test/PerChainTransceiversDemo.t.sol +++ b/evm/test/PerChainTransceiversDemo.t.sol @@ -188,27 +188,19 @@ contract TestPerChainTransceiversDemo is Test, IRateLimiterEvents { chainId1, bytes32(uint256(uint160(address(chainOneSecondTransceiver)))) ); - // Set the default thresholds. ////////////////////////////////////////////////////////////////////// - nttManagerChain1.setThreshold(2); - nttManagerChain2.setThreshold(2); - // Set up our per-chain transceivers and thresholds. //////////////////////////////////////////////// // Set up chain one. - // 1.a: - nttManagerChain1.enableSendTransceiverForChain(address(chainOneFirstTransceiver), chainId2); - nttManagerChain1.enableSendTransceiverForChain(address(chainOneSecondTransceiver), chainId2); - // 2.b: - nttManagerChain1.enableRecvTransceiverForChain(address(chainOneFirstTransceiver), chainId2); - nttManagerChain1.setPerChainThreshold(chainId2, 1); + // 1.a: Enable both transceivers for sending. + nttManagerChain1.setSendTransceiverBitmapForChain(chainId2, 0x03); + // 2.b: Enable the first transceiver for receiving with a threshold of one. + nttManagerChain1.setRecvTransceiverBitmapForChain(chainId2, 0x01, 1); // Set up chain two. - // 2.a: - nttManagerChain2.enableSendTransceiverForChain(address(chainTwoFirstTransceiver), chainId1); - // 1.b: - nttManagerChain2.enableRecvTransceiverForChain(address(chainTwoFirstTransceiver), chainId1); - nttManagerChain2.enableRecvTransceiverForChain(address(chainTwoSecondTransceiver), chainId1); - nttManagerChain2.setPerChainThreshold(chainId1, 2); + // 2.a: Enable first transceiver for sending. + nttManagerChain2.setSendTransceiverBitmapForChain(chainId1, 0x01); + // 1.b: Enable both transceivers for receiving with a threshold of two. + nttManagerChain2.setRecvTransceiverBitmapForChain(chainId1, 0x03, 2); } function test_verifyConfig() public view { @@ -229,9 +221,8 @@ contract TestPerChainTransceiversDemo is Test, IRateLimiterEvents { nttManagerChain1.getEnabledRecvTransceiversForChain(chainId2) == 0x1, "On chain 1, only first transceiver should be enabled for receiving from chain 2" ); - require(nttManagerChain1.getThreshold() == 2, "On chain 1, the default threshold is wrong"); require( - nttManagerChain1.getPerChainThreshold(chainId2) == 1, + nttManagerChain1.getThresholdForChain(chainId2) == 1, "On chain 1, threshold for chain 2 is wrong" ); @@ -252,9 +243,8 @@ contract TestPerChainTransceiversDemo is Test, IRateLimiterEvents { nttManagerChain2.getEnabledRecvTransceiversForChain(chainId1) == 0x3, "On chain 2, both transceivers should be enabled for receiving from chain 1" ); - require(nttManagerChain2.getThreshold() == 2, "On chain 2, the default threshold is wrong"); require( - nttManagerChain2.getPerChainThreshold(chainId1) == 2, + nttManagerChain2.getThresholdForChain(chainId1) == 2, "On chain 2, threshold for chain 1 is wrong" ); } From dd0400d7654a44c0be9973e2f8d0f02b38f2c21c Mon Sep 17 00:00:00 2001 From: Bruce Riley Date: Wed, 2 Oct 2024 14:19:51 -0500 Subject: [PATCH 09/10] EVM: Remove NttManagerWithPerChainTransceivers from IManagerBase --- evm/src/NttManager/ManagerBase.sol | 51 ------------ .../NttManagerWithPerChainTransceivers.sol | 80 +++++++++++++------ evm/src/interfaces/IManagerBase.sol | 74 ----------------- evm/test/PerChainTransceivers.t.sol | 32 ++++---- 4 files changed, 70 insertions(+), 167 deletions(-) diff --git a/evm/src/NttManager/ManagerBase.sol b/evm/src/NttManager/ManagerBase.sol index f303190c2..5f0c76f75 100644 --- a/evm/src/NttManager/ManagerBase.sol +++ b/evm/src/NttManager/ManagerBase.sol @@ -420,57 +420,6 @@ abstract contract ManagerBase is emit ThresholdChanged(oldThreshold, threshold); } - /// @inheritdoc IManagerBase - function getSendTransceiverBitmapForChain( - uint16 // forChainId - ) external view virtual returns (uint64) { - return 0; - } - - /// @inheritdoc IManagerBase - function getRecvTransceiverBitmapForChain( - uint16 // forChainId - ) external view virtual returns (uint64) { - return 0; - } - - /// @inheritdoc IManagerBase - function getThresholdForChain( - uint16 // forChainId - ) external view virtual returns (uint8) { - return 0; - } - - /// @inheritdoc IManagerBase - function getChainsEnabledForSending() external view virtual returns (uint16[] memory) {} - - /// @inheritdoc IManagerBase - function getChainsEnabledForReceiving() external view virtual returns (uint16[] memory) {} - - /// @inheritdoc IManagerBase - function setSendTransceiverBitmapForChain( - uint16, // forChainId - uint64 // indexBitmap - ) external virtual onlyOwner { - revert NotImplemented(); - } - - /// @inheritdoc IManagerBase - function setRecvTransceiverBitmapForChain( - uint16, // forChainId - uint64, // indexBitmap - uint8 // threshold - ) external virtual onlyOwner { - revert NotImplemented(); - } - - /// @inheritdoc IManagerBase - function setTransceiversForChains( - SetTransceiversForChainEntry[] memory // params - ) external virtual onlyOwner { - revert NotImplemented(); - } - // =============== Internal ============================================================== function _setTransceiverAttestedToMessage(bytes32 digest, uint8 index) internal { diff --git a/evm/src/NttManager/NttManagerWithPerChainTransceivers.sol b/evm/src/NttManager/NttManagerWithPerChainTransceivers.sol index 9be45a5aa..16831974a 100644 --- a/evm/src/NttManager/NttManagerWithPerChainTransceivers.sol +++ b/evm/src/NttManager/NttManagerWithPerChainTransceivers.sol @@ -14,6 +14,26 @@ import "./NttManagerNoRateLimiting.sol"; /// /// @dev All of the developer notes from `NttManager` apply here. contract NttManagerWithPerChainTransceivers is NttManagerNoRateLimiting { + /// @notice Emitted when the sending transceivers are updated for a chain. + /// @dev Topic0 + /// 0xe3bed59083cdad1d552b8eef7d3acc80adb78da6c6f375ae3adf5cb4823b2619 + /// @param chainId The chain that was updated. + /// @param oldBitmap The original index bitmap. + /// @param oldBitmap The updated index bitmap. + event SendTransceiversUpdatedForChain(uint16 chainId, uint64 oldBitmap, uint64 newBitmap); + + /// @notice Emitted when the receivinging transceivers are updated for a chain. + /// @dev Topic0 + /// 0xd09fdac2bd3e794a578992bfe77134765623d22a2b3201e2994f681828160f2f + /// @param chainId The chain that was updated. + /// @param oldBitmap The original index bitmap. + /// @param oldBitmap The updated index bitmap. + /// @param oldThreshold The original receive threshold. + /// @param newThreshold The updated receive threshold. + event RecvTransceiversUpdatedForChain( + uint16 chainId, uint64 oldBitmap, uint64 newBitmap, uint8 oldThreshold, uint8 newThreshold + ); + /// @notice Transceiver index is greater than the number of enabled transceivers. /// @dev Selector 0x770c2d3c. /// @param index The transceiver index that is invalid. @@ -38,6 +58,14 @@ contract NttManagerWithPerChainTransceivers is NttManagerNoRateLimiting { /// @param transceiver The address of the transceiver that is not enabled. error TransceiverNotEnabled(uint8 index, address transceiver); + /// @notice The structure of a per-chain entry in the call to setTransceiversForChains. + struct SetTransceiversForChainEntry { + uint64 sendBitmap; + uint64 recvBitmap; + uint16 chainId; + uint8 recvThreshold; + } + constructor( address _token, Mode _mode, @@ -60,54 +88,50 @@ contract NttManagerWithPerChainTransceivers is NttManagerNoRateLimiting { // =============== Public Getters ======================================================== - /// @inheritdoc IManagerBase + /// @notice Returns the bitmap of send transceivers enabled for a chain. + /// @param forChainId The chain for which sending is enabled. function getSendTransceiverBitmapForChain( uint16 forChainId - ) external view override(ManagerBase, IManagerBase) returns (uint64) { + ) external view returns (uint64) { return _getPerChainTransceiverBitmap(forChainId, SEND_TRANSCEIVER_BITMAP_SLOT); } - /// @inheritdoc IManagerBase + /// @notice Returns the bitmap of receive transceivers enabled for a chain. + /// @param forChainId The chain for which receiving is enabled. function getRecvTransceiverBitmapForChain( uint16 forChainId - ) external view override(ManagerBase, IManagerBase) returns (uint64) { + ) external view returns (uint64) { return _getPerChainTransceiverBitmap(forChainId, RECV_TRANSCEIVER_BITMAP_SLOT); } - /// @inheritdoc IManagerBase - function getChainsEnabledForSending() - external - pure - override(ManagerBase, IManagerBase) - returns (uint16[] memory) - { + /// @notice Returns the set of chains for which sending is enabled. + function getChainsEnabledForSending() external pure returns (uint16[] memory) { return _getEnabledChainsStorage(SEND_ENABLED_CHAINS_SLOT); } - /// @inheritdoc IManagerBase - function getChainsEnabledForReceiving() - external - pure - override(ManagerBase, IManagerBase) - returns (uint16[] memory) - { + /// @notice Returns the set of chains for which receiving is enabled. + function getChainsEnabledForReceiving() external pure returns (uint16[] memory) { return _getEnabledChainsStorage(RECV_ENABLED_CHAINS_SLOT); } - /// @inheritdoc IManagerBase + /// @notice Returns the number of Transceivers that must attest to a msgId for + /// it to be considered valid and acted upon. + /// @param forChainId The chain for which the threshold applies. function getThresholdForChain( uint16 forChainId - ) public view override(ManagerBase, IManagerBase) returns (uint8) { + ) public view returns (uint8) { return _getThresholdStoragePerChain()[forChainId].num; } // =============== Public Setters ======================================================== - /// @inheritdoc IManagerBase + /// @notice Sets the bitmap of transceivers enabled for sending for a chain. + /// @param forChainId The chain to be updated. + /// @param indexBitmap The bitmap of transceiver indexes that are enabled. function setSendTransceiverBitmapForChain( uint16 forChainId, uint64 indexBitmap - ) public override(ManagerBase, IManagerBase) onlyOwner { + ) public onlyOwner { _validateTransceivers(indexBitmap); mapping(uint16 => _EnabledTransceiverBitmap) storage _bitmaps = _getPerChainTransceiverBitmapStorage(SEND_TRANSCEIVER_BITMAP_SLOT); @@ -123,12 +147,15 @@ contract NttManagerWithPerChainTransceivers is NttManagerNoRateLimiting { emit SendTransceiversUpdatedForChain(forChainId, oldBitmap, indexBitmap); } - /// @inheritdoc IManagerBase + /// @notice Sets the bitmap of transceivers enabled for receiving for a chain. + /// @param forChainId The chain to be updated. + /// @param indexBitmap The bitmap of transceiver indexes that are enabled. + /// @param threshold The receive threshold for the chain. function setRecvTransceiverBitmapForChain( uint16 forChainId, uint64 indexBitmap, uint8 threshold - ) public override(ManagerBase, IManagerBase) onlyOwner { + ) public onlyOwner { _validateTransceivers(indexBitmap); // Validate the threshold against the bitmap. @@ -164,10 +191,11 @@ contract NttManagerWithPerChainTransceivers is NttManagerNoRateLimiting { ); } - /// @inheritdoc IManagerBase + /// @notice Sets the transceiver bitmaps and thresholds for a set of chains. + /// @param params The values to be applied for a set of chains. function setTransceiversForChains( SetTransceiversForChainEntry[] memory params - ) external override(ManagerBase, IManagerBase) onlyOwner { + ) external onlyOwner { for (uint256 idx = 0; idx < params.length; idx++) { setSendTransceiverBitmapForChain(params[idx].chainId, params[idx].sendBitmap); setRecvTransceiverBitmapForChain( diff --git a/evm/src/interfaces/IManagerBase.sol b/evm/src/interfaces/IManagerBase.sol index 92562dcb7..46fae3628 100644 --- a/evm/src/interfaces/IManagerBase.sol +++ b/evm/src/interfaces/IManagerBase.sol @@ -34,14 +34,6 @@ interface IManagerBase { uint8 num; } - /// @notice The structure of a per-chain entry in the call to setTransceiversForChains. - struct SetTransceiversForChainEntry { - uint64 sendBitmap; - uint64 recvBitmap; - uint16 chainId; - uint8 recvThreshold; - } - /// @notice Emitted when a message has been attested to. /// @dev Topic0 /// 0x35a2101eaac94b493e0dfca061f9a7f087913fde8678e7cde0aca9897edba0e5. @@ -72,26 +64,6 @@ interface IManagerBase { /// @param threshold The current threshold of transceivers. event TransceiverRemoved(address transceiver, uint8 threshold); - /// @notice Emitted when the sending transceivers are updated for a chain. - /// @dev Topic0 - /// 0xe3bed59083cdad1d552b8eef7d3acc80adb78da6c6f375ae3adf5cb4823b2619 - /// @param chainId The chain that was updated. - /// @param oldBitmap The original index bitmap. - /// @param oldBitmap The updated index bitmap. - event SendTransceiversUpdatedForChain(uint16 chainId, uint64 oldBitmap, uint64 newBitmap); - - /// @notice Emitted when the receivinging transceivers are updated for a chain. - /// @dev Topic0 - /// 0xd09fdac2bd3e794a578992bfe77134765623d22a2b3201e2994f681828160f2f - /// @param chainId The chain that was updated. - /// @param oldBitmap The original index bitmap. - /// @param oldBitmap The updated index bitmap. - /// @param oldThreshold The original receive threshold. - /// @param newThreshold The updated receive threshold. - event RecvTransceiversUpdatedForChain( - uint16 chainId, uint64 oldBitmap, uint64 newBitmap, uint8 oldThreshold, uint8 newThreshold - ); - /// @notice payment for a transfer is too low. /// @param requiredPayment The required payment. /// @param providedPayment The provided payment. @@ -234,50 +206,4 @@ interface IManagerBase { /// @notice Returns the chain ID. function chainId() external view returns (uint16); - - /// @notice Returns the bitmap of send transceivers enabled for a chain. - /// @param forChainId The chain for which sending is enabled. - function getSendTransceiverBitmapForChain( - uint16 forChainId - ) external view returns (uint64); - - /// @notice Returns the bitmap of receive transceivers enabled for a chain. - /// @param forChainId The chain for which receiving is enabled. - function getRecvTransceiverBitmapForChain( - uint16 forChainId - ) external view returns (uint64); - - /// @notice Returns the set of chains for which sending is enabled. - function getChainsEnabledForSending() external view returns (uint16[] memory); - - /// @notice Returns the set of chains for which receiving is enabled. - function getChainsEnabledForReceiving() external view returns (uint16[] memory); - - /// @notice Sets the bitmap of transceivers enabled for sending for a chain. - /// @param forChainId The chain to be updated. - /// @param indexBitmap The bitmap of transceiver indexes that are enabled. - function setSendTransceiverBitmapForChain(uint16 forChainId, uint64 indexBitmap) external; - - /// @notice Sets the bitmap of transceivers enabled for receiving for a chain. - /// @param forChainId The chain to be updated. - /// @param indexBitmap The bitmap of transceiver indexes that are enabled. - /// @param threshold The receive threshold for the chain. - function setRecvTransceiverBitmapForChain( - uint16 forChainId, - uint64 indexBitmap, - uint8 threshold - ) external; - - /// @notice Sets the transceiver bitmaps and thresholds for a set of chains. - /// @param params The values to be applied for a set of chains. - function setTransceiversForChains( - SetTransceiversForChainEntry[] memory params - ) external; - - /// @notice Returns the number of Transceivers that must attest to a msgId for - /// it to be considered valid and acted upon. - /// @param forChainId The chain for which the threshold applies. - function getThresholdForChain( - uint16 forChainId - ) external view returns (uint8); } diff --git a/evm/test/PerChainTransceivers.t.sol b/evm/test/PerChainTransceivers.t.sol index a91376f83..08bad4da2 100755 --- a/evm/test/PerChainTransceivers.t.sol +++ b/evm/test/PerChainTransceivers.t.sol @@ -436,17 +436,17 @@ contract TestPerChainTransceivers is Test, IRateLimiterEvents { } function test_setTransceiversForChains() public { - IManagerBase.SetTransceiversForChainEntry[] memory params = - new IManagerBase.SetTransceiversForChainEntry[](2); + NttManagerWithPerChainTransceivers.SetTransceiversForChainEntry[] memory params = + new NttManagerWithPerChainTransceivers.SetTransceiversForChainEntry[](2); - params[0] = IManagerBase.SetTransceiversForChainEntry({ + params[0] = NttManagerWithPerChainTransceivers.SetTransceiversForChainEntry({ chainId: chainId2, sendBitmap: 0x02, recvBitmap: 0x01, recvThreshold: 1 }); - params[1] = IManagerBase.SetTransceiversForChainEntry({ + params[1] = NttManagerWithPerChainTransceivers.SetTransceiversForChainEntry({ chainId: chainId3, sendBitmap: 0x02, recvBitmap: 0x03, @@ -515,10 +515,10 @@ contract TestPerChainTransceivers is Test, IRateLimiterEvents { // This test does a transfer between chain one and chain two. // Since the receive thresholds are set to one, posting a VAA from only one transceiver completes the transfer. function test_thresholdLessThanNumReceivers() public { - IManagerBase.SetTransceiversForChainEntry[] memory nttManager1Params = - new IManagerBase.SetTransceiversForChainEntry[](1); + NttManagerWithPerChainTransceivers.SetTransceiversForChainEntry[] memory nttManager1Params = + new NttManagerWithPerChainTransceivers.SetTransceiversForChainEntry[](1); - nttManager1Params[0] = IManagerBase.SetTransceiversForChainEntry({ + nttManager1Params[0] = NttManagerWithPerChainTransceivers.SetTransceiversForChainEntry({ chainId: chainId2, sendBitmap: 0x03, recvBitmap: 0x03, @@ -527,10 +527,10 @@ contract TestPerChainTransceivers is Test, IRateLimiterEvents { nttManagerChain1.setTransceiversForChains(nttManager1Params); - IManagerBase.SetTransceiversForChainEntry[] memory nttManager2Params = - new IManagerBase.SetTransceiversForChainEntry[](1); + NttManagerWithPerChainTransceivers.SetTransceiversForChainEntry[] memory nttManager2Params = + new NttManagerWithPerChainTransceivers.SetTransceiversForChainEntry[](1); - nttManager2Params[0] = IManagerBase.SetTransceiversForChainEntry({ + nttManager2Params[0] = NttManagerWithPerChainTransceivers.SetTransceiversForChainEntry({ chainId: chainId1, sendBitmap: 0x03, recvBitmap: 0x03, @@ -658,10 +658,10 @@ contract TestPerChainTransceivers is Test, IRateLimiterEvents { // This test does a transfer between chain one and chain three. // Since the threshold for these two chains is two, the transfer is not completed until both VAAs are posted. function test_thresholdEqualToNumberOfReceivers() public { - IManagerBase.SetTransceiversForChainEntry[] memory nttManager1Params = - new IManagerBase.SetTransceiversForChainEntry[](1); + NttManagerWithPerChainTransceivers.SetTransceiversForChainEntry[] memory nttManager1Params = + new NttManagerWithPerChainTransceivers.SetTransceiversForChainEntry[](1); - nttManager1Params[0] = IManagerBase.SetTransceiversForChainEntry({ + nttManager1Params[0] = NttManagerWithPerChainTransceivers.SetTransceiversForChainEntry({ chainId: chainId3, sendBitmap: 0x03, recvBitmap: 0x03, @@ -670,10 +670,10 @@ contract TestPerChainTransceivers is Test, IRateLimiterEvents { nttManagerChain1.setTransceiversForChains(nttManager1Params); - IManagerBase.SetTransceiversForChainEntry[] memory nttManager3Params = - new IManagerBase.SetTransceiversForChainEntry[](1); + NttManagerWithPerChainTransceivers.SetTransceiversForChainEntry[] memory nttManager3Params = + new NttManagerWithPerChainTransceivers.SetTransceiversForChainEntry[](1); - nttManager3Params[0] = IManagerBase.SetTransceiversForChainEntry({ + nttManager3Params[0] = NttManagerWithPerChainTransceivers.SetTransceiversForChainEntry({ chainId: chainId1, sendBitmap: 0x03, recvBitmap: 0x03, From 0bff03c23707ca58cb5d041e4de333b8982ffeef Mon Sep 17 00:00:00 2001 From: Bruce Riley Date: Tue, 8 Oct 2024 16:11:36 -0500 Subject: [PATCH 10/10] EVM: per-chain transceiver code review rework --- .../NttManagerWithPerChainTransceivers.sol | 99 ++++++++++--------- evm/src/NttManager/TransceiverRegistry.sol | 2 +- evm/test/PerChainTransceivers.t.sol | 14 ++- 3 files changed, 67 insertions(+), 48 deletions(-) diff --git a/evm/src/NttManager/NttManagerWithPerChainTransceivers.sol b/evm/src/NttManager/NttManagerWithPerChainTransceivers.sol index 16831974a..f037457bc 100644 --- a/evm/src/NttManager/NttManagerWithPerChainTransceivers.sol +++ b/evm/src/NttManager/NttManagerWithPerChainTransceivers.sol @@ -34,17 +34,10 @@ contract NttManagerWithPerChainTransceivers is NttManagerNoRateLimiting { uint16 chainId, uint64 oldBitmap, uint64 newBitmap, uint8 oldThreshold, uint8 newThreshold ); - /// @notice Transceiver index is greater than the number of enabled transceivers. - /// @dev Selector 0x770c2d3c. + /// @notice Transceiver index does not match one in the list. + /// @dev Selector 0x24595b41. /// @param index The transceiver index that is invalid. - /// @param len The length of the transceiver list. - error TransceiverIndexTooLarge(uint8 index, uint256 len); - - /// @notice Transceiver index does not match the one in the list. - /// @dev Selector 0x2f52d3e. - /// @param index The transceiver index that is invalid. - /// @param expectedIndex The index in the transceiver list. - error InvalidTransceiverIndex(uint8 index, uint8 expectedIndex); + error InvalidTransceiverIndex(uint8 index); /// @notice Transceiver with specified index is not registered. /// @dev Selector 0x38ab702a. @@ -196,11 +189,15 @@ contract NttManagerWithPerChainTransceivers is NttManagerNoRateLimiting { function setTransceiversForChains( SetTransceiversForChainEntry[] memory params ) external onlyOwner { - for (uint256 idx = 0; idx < params.length; idx++) { + uint256 len = params.length; + for (uint256 idx = 0; idx < len;) { setSendTransceiverBitmapForChain(params[idx].chainId, params[idx].sendBitmap); setRecvTransceiverBitmapForChain( params[idx].chainId, params[idx].recvBitmap, params[idx].recvThreshold ); + unchecked { + ++idx; + } } } @@ -212,7 +209,7 @@ contract NttManagerWithPerChainTransceivers is NttManagerNoRateLimiting { ) internal view override returns (bool) { uint64 bitmap = _getPerChainTransceiverBitmap(forChainId, SEND_TRANSCEIVER_BITMAP_SLOT); uint8 index = _getTransceiverInfosStorage()[transceiver].index; - return (bitmap & uint64(1 << index)) != 0; + return (bitmap & uint64(1 << index)) > 0; } /// @inheritdoc IManagerBase @@ -234,6 +231,24 @@ contract NttManagerWithPerChainTransceivers is NttManagerNoRateLimiting { return attInfo.attestedTransceivers & enabledTransceiverBitmap & enabledTransceiversForChain; } + function _checkTransceiversInvariants() internal view override { + super._checkTransceiversInvariants(); + _validateTransceivers(SEND_ENABLED_CHAINS_SLOT, SEND_TRANSCEIVER_BITMAP_SLOT); + _validateTransceivers(RECV_ENABLED_CHAINS_SLOT, RECV_TRANSCEIVER_BITMAP_SLOT); + } + + function _validateTransceivers(bytes32 chainsTag, bytes32 bitmapTag) private view { + uint16[] memory chains = _getEnabledChainsStorage(chainsTag); + uint256 len = chains.length; + for (uint256 idx = 0; idx < len;) { + uint64 bitmap = _getPerChainTransceiverBitmap(chains[idx], bitmapTag); + _validateTransceivers(bitmap); + unchecked { + ++idx; + } + } + } + // ==================== Implementation ========================= function _getEnabledRecvTransceiversForChain( @@ -286,65 +301,59 @@ contract NttManagerWithPerChainTransceivers is NttManagerNoRateLimiting { function _validateTransceiver( uint8 index ) internal view { - address[] storage _enabledTransceivers = _getEnabledTransceiversStorage(); - if (index >= _enabledTransceivers.length) { - revert TransceiverIndexTooLarge(index, _enabledTransceivers.length); - } - - address transceiverAddr = _enabledTransceivers[index]; mapping(address => TransceiverInfo) storage transceiverInfos = _getTransceiverInfosStorage(); + address[] storage _enabledTransceivers = _getEnabledTransceiversStorage(); + uint256 len = _enabledTransceivers.length; + for (uint256 idx = 0; (idx < len);) { + address transceiverAddr = _enabledTransceivers[idx]; + if (transceiverInfos[transceiverAddr].index == index) { + if (!transceiverInfos[transceiverAddr].registered) { + revert TransceiverNotRegistered(index, transceiverAddr); + } - if (transceiverInfos[transceiverAddr].index != index) { - revert InvalidTransceiverIndex(index, transceiverInfos[transceiverAddr].index); - } - - if (!transceiverInfos[transceiverAddr].registered) { - revert TransceiverNotRegistered(index, transceiverAddr); + if (!transceiverInfos[transceiverAddr].enabled) { + revert TransceiverNotEnabled(index, transceiverAddr); + } + // This index is good. + return; + } + unchecked { + ++idx; + } } - if (!transceiverInfos[transceiverAddr].enabled) { - revert TransceiverNotEnabled(index, transceiverAddr); - } + revert InvalidTransceiverIndex(index); } function _removeChain(bytes32 tag, uint16 forChainId) private { uint16[] storage chains = _getEnabledChainsStorage(tag); uint256 len = chains.length; - for (uint256 idx = 0; (idx < len); idx++) { + for (uint256 idx = 0; (idx < len);) { if (chains[idx] == forChainId) { - if (len > 1) { - chains[idx] = chains[len - 1]; - } + chains[idx] = chains[len - 1]; chains.pop(); return; } + unchecked { + ++idx; + } } } function _addChainIfNeeded(bytes32 tag, uint16 forChainId) private { uint16[] storage chains = _getEnabledChainsStorage(tag); - uint256 zeroIdx = type(uint256).max; uint256 len = chains.length; - for (uint256 idx = 0; (idx < len); idx++) { + for (uint256 idx = 0; (idx < len);) { if (chains[idx] == forChainId) { return; } - if (chains[idx] == 0) { - zeroIdx = idx; + unchecked { + ++idx; } } - - if (zeroIdx != type(uint256).max) { - chains[zeroIdx] = forChainId; - } else { - chains.push(forChainId); - } + chains.push(forChainId); } - // function _copyEnabledChains(bytes32 tag) uint16[] memory { - // uint16[] ret = new - // } - function _getEnabledChainsStorage( bytes32 tag ) internal pure returns (uint16[] storage $) { diff --git a/evm/src/NttManager/TransceiverRegistry.sol b/evm/src/NttManager/TransceiverRegistry.sol index d95f39b73..07b17bccf 100644 --- a/evm/src/NttManager/TransceiverRegistry.sol +++ b/evm/src/NttManager/TransceiverRegistry.sol @@ -257,7 +257,7 @@ abstract contract TransceiverRegistry { /// @dev Check that the transceiver nttManager is in a valid state. /// Checking these invariants is somewhat costly, but we only need to do it /// when modifying the transceivers, which happens infrequently. - function _checkTransceiversInvariants() internal view { + function _checkTransceiversInvariants() internal view virtual { _NumTransceivers storage _numTransceivers = _getNumTransceiversStorage(); address[] storage _enabledTransceivers = _getEnabledTransceiversStorage(); diff --git a/evm/test/PerChainTransceivers.t.sol b/evm/test/PerChainTransceivers.t.sol index 08bad4da2..306b59450 100755 --- a/evm/test/PerChainTransceivers.t.sol +++ b/evm/test/PerChainTransceivers.t.sol @@ -503,13 +503,23 @@ contract TestPerChainTransceivers is Test, IRateLimiterEvents { vm.expectRevert( abi.encodeWithSelector( - NttManagerWithPerChainTransceivers.TransceiverIndexTooLarge.selector, 6, 2 + NttManagerWithPerChainTransceivers.InvalidTransceiverIndex.selector, 6 ) ); nttManagerChain1.setRecvTransceiverBitmapForChain(chainId2, 0x40, 0); + // Can't globally remove a transceiver when it's enabled for a chain. + nttManagerChain1.setRecvTransceiverBitmapForChain(chainId2, 0x02, 1); + vm.expectRevert( + abi.encodeWithSelector( + NttManagerWithPerChainTransceivers.InvalidTransceiverIndex.selector, 1 + ) + ); + nttManagerChain1.removeTransceiver(address(secondWormholeTransceiverChain1)); + + // But once you disable it per-chain, you should be able to remove it globally. nttManagerChain1.setRecvTransceiverBitmapForChain(chainId2, 0, 0); - nttManagerChain1.removeTransceiver(address(wormholeTransceiverChain1)); + nttManagerChain1.removeTransceiver(address(secondWormholeTransceiverChain1)); } // This test does a transfer between chain one and chain two.