diff --git a/protocol/synthetix/cannonfile.test.toml b/protocol/synthetix/cannonfile.test.toml index 3a09ae1a94..da992a1b1c 100644 --- a/protocol/synthetix/cannonfile.test.toml +++ b/protocol/synthetix/cannonfile.test.toml @@ -64,6 +64,12 @@ artifact = "contracts/generated/test/TestableVaultEpochStorage.sol:TestableVault [contract.TestableVaultStorage] artifact = "contracts/generated/test/TestableVaultStorage.sol:TestableVaultStorage" +[contract.CallSelfModule] +artifact = "contracts/mocks/CallSelfModule.sol:CallSelfModule" + +[contract.FakeSendCrossChainModule] +artifact = "contracts/mocks/FakeSendCrossChainModule.sol:FakeSendCrossChainModule" + # Core [router.CoreRouter] contracts = [ @@ -89,6 +95,8 @@ contracts = [ "RewardsManagerModule", "UtilsModule", "VaultModule", + "CallSelfModule", + "FakeSendCrossChainModule", "TestableAccountStorage", "TestableAccountRBACStorage", "TestableCollateralStorage", @@ -133,3 +141,19 @@ args = [ "0x7d632bd2<%= defaultAbiCoder.encode(['bytes32', 'bool'], [formatBytes32String('createPool'), true]).slice(2) %>", ] ] + +# cross chain wormhole +[import.wormhole] +source = "erc7412-wormhole:latest" + +[invoke.configureWormholeCrossChain] +target = ["CoreProxy"] +fromCall.func = "owner" +func = "configureWormholeCrossChain" +args = [ + "<%= imports.wormhole.imports.wormhole.contracts.Wormhole.address %>", + "<%= imports.wormhole.imports.wormhole.contracts.Wormhole.address %>", + "<%= imports.wormhole.contracts.WormholeERC7412Receiver.address %>", + [1, 13370], + [1, 13370] +] diff --git a/protocol/synthetix/cannonfile.toml b/protocol/synthetix/cannonfile.toml index f549050a21..21b7701da7 100644 --- a/protocol/synthetix/cannonfile.toml +++ b/protocol/synthetix/cannonfile.toml @@ -208,6 +208,9 @@ args = [ "0x7d632bd2<%= defaultAbiCoder.encode(['bytes32', 'bool'], [formatBytes32String('withdrawMarketUsd'), true]).slice(2) %>", "0x7d632bd2<%= defaultAbiCoder.encode(['bytes32', 'bool'], [formatBytes32String('claimRewards'), true]).slice(2) %>", "0x7d632bd2<%= defaultAbiCoder.encode(['bytes32', 'bool'], [formatBytes32String('delegateCollateral'), true]).slice(2) %>", + "0x7d632bd2<%= defaultAbiCoder.encode(['bytes32', 'bool'], [formatBytes32String('createCrossChainPool'), true]).slice(2) %>", + "0x7d632bd2<%= defaultAbiCoder.encode(['bytes32', 'bool'], [formatBytes32String('setCrossChainPoolSelectors'), true]).slice(2) %>", + "0x7d632bd2<%= defaultAbiCoder.encode(['bytes32', 'bool'], [formatBytes32String('setCrossChainPoolConfiguration'), true]).slice(2) %>", ] ] diff --git a/protocol/synthetix/contracts/interfaces/ICrossChainPoolModule.sol b/protocol/synthetix/contracts/interfaces/ICrossChainPoolModule.sol index 33fc274396..6fb527e39c 100644 --- a/protocol/synthetix/contracts/interfaces/ICrossChainPoolModule.sol +++ b/protocol/synthetix/contracts/interfaces/ICrossChainPoolModule.sol @@ -19,6 +19,12 @@ interface ICrossChainPoolModule { uint128 thisChainPoolId ); + function setCrossChainPoolSelectors( + uint128 poolId, + bytes4 readSelector, + bytes4 writeSelector + ) external; + function createCrossChainPool( uint128 sourcePoolId, uint64 targetChainId diff --git a/protocol/synthetix/contracts/interfaces/IOffchainWormholeModule.sol b/protocol/synthetix/contracts/interfaces/IOffchainWormholeModule.sol index 4869f58491..b05d9980dc 100644 --- a/protocol/synthetix/contracts/interfaces/IOffchainWormholeModule.sol +++ b/protocol/synthetix/contracts/interfaces/IOffchainWormholeModule.sol @@ -2,7 +2,7 @@ pragma solidity >=0.8.11 <0.9.0; import "./external/IWormholeRelayer.sol"; -import "./external/IWormholeCrossChainRead.sol"; +import "./external/IWormholeERC7412Receiver.sol"; /** * @title Module with assorted utility functions. @@ -18,12 +18,24 @@ interface IOffchainWormholeModule { * @param supportedNetworks The list of supported chain ids for the cross chain protocol * @param selectors Mapping of selectors used for wormhole related to the supportedNetworks */ - function configureWormholeCrossChain( IWormholeRelayerSend send, IWormholeRelayerDelivery recv, - IWormholeCrossChainRead read, + IWormholeERC7412Receiver read, uint64[] memory supportedNetworks, uint16[] memory selectors ) external; + + function readCrossChainWormhole( + bytes32 /* subscriptionId */, + uint64[] memory chains, + bytes memory call, + uint256 /* gasLimit */ + ) external returns (bytes[] memory responses); + + function sendWormholeMessage( + uint64[] memory chainIds, + bytes memory message, + uint256 gasLimit + ) external returns (bytes32[] memory sequenceNumbers); } diff --git a/protocol/synthetix/contracts/interfaces/external/IERC7412.sol b/protocol/synthetix/contracts/interfaces/external/IERC7412.sol new file mode 100644 index 0000000000..daf5dc76d0 --- /dev/null +++ b/protocol/synthetix/contracts/interfaces/external/IERC7412.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.11 <0.9.0; + +interface IERC7412 { + error FeeRequired(uint amount); + error OracleDataRequired(address oracleContract, bytes oracleQuery); + + function oracleId() external view returns (bytes32 oracleId); + + function fulfillOracleQuery(bytes calldata signedOffchainData) external payable; +} diff --git a/protocol/synthetix/contracts/interfaces/external/IWormholeERC7412Receiver.sol b/protocol/synthetix/contracts/interfaces/external/IWormholeERC7412Receiver.sol new file mode 100644 index 0000000000..c4678bee2b --- /dev/null +++ b/protocol/synthetix/contracts/interfaces/external/IWormholeERC7412Receiver.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.11 <0.9.0; + +import "@synthetixio/core-contracts/contracts/interfaces/IERC20.sol"; +import "./IERC7412.sol"; + +interface IWormholeERC7412Receiver is IERC7412 { + struct CrossChainRequest { + uint64 chainSelector; + uint256 timestamp; + address target; + bytes data; + } + + function getCrossChainData( + CrossChainRequest[] memory reqs, + uint256 maxAge + ) external view returns (bytes[] memory); +} diff --git a/protocol/synthetix/contracts/mocks/CallSelfModule.sol b/protocol/synthetix/contracts/mocks/CallSelfModule.sol new file mode 100644 index 0000000000..ce63d87964 --- /dev/null +++ b/protocol/synthetix/contracts/mocks/CallSelfModule.sol @@ -0,0 +1,15 @@ +//SPDX-License-Identifier: MIT +pragma solidity >=0.8.11 <0.9.0; + +import "@synthetixio/core-contracts/contracts/utils/CallUtil.sol"; + +/** + * A module which can be added to the core system to allow it to call itself. Used for cross chain purposes, which rely on messages to self + */ +contract CallSelfModule { + using CallUtil for address; + + function callSelf(bytes memory selfCallData) external returns (bytes memory) { + return address(this).tryCall(selfCallData); + } +} diff --git a/protocol/synthetix/contracts/mocks/FakeSendCrossChainModule.sol b/protocol/synthetix/contracts/mocks/FakeSendCrossChainModule.sol new file mode 100644 index 0000000000..b215a70e99 --- /dev/null +++ b/protocol/synthetix/contracts/mocks/FakeSendCrossChainModule.sol @@ -0,0 +1,17 @@ +//SPDX-License-Identifier: MIT +pragma solidity >=0.8.11 <0.9.0; + +import "@synthetixio/core-contracts/contracts/utils/CallUtil.sol"; + +/** + * A module which can be added to the core system to allow it to call itself. Used for cross chain purposes, which rely on messages to self + */ +contract FakeSendCrossChainModule { + function sendCrossChainFake( + uint64[] memory targetChains, + bytes memory data, + uint256 gasLimit + ) external returns (bytes32[] memory) { + return new bytes32[](targetChains.length); + } +} diff --git a/protocol/synthetix/contracts/mocks/FakeWormholeCrossChainRead.sol b/protocol/synthetix/contracts/mocks/FakeWormholeCrossChainRead.sol new file mode 100644 index 0000000000..8bc08d3d7d --- /dev/null +++ b/protocol/synthetix/contracts/mocks/FakeWormholeCrossChainRead.sol @@ -0,0 +1,37 @@ +//SPDX-License-Identifier: MIT +pragma solidity >=0.8.11 <0.9.0; + +import "../interfaces/external/IWormholeERC7412Receiver.sol"; + +contract FakeWormholeCrossChainRead is IWormholeERC7412Receiver { + mapping(bytes32 => bytes) public queryResponses; + + function getCrossChainData( + IWormholeERC7412Receiver.CrossChainRequest[] memory reqs, + uint256 maxAge + ) external view override returns (bytes[] memory responses) { + responses = new bytes[](reqs.length); + for (uint i = 0; i < reqs.length; i++) { + CrossChainRequest memory req = reqs[i]; + bytes32 reqHash = keccak256(abi.encodePacked(req.chainSelector, req.target, req.data)); + + responses[i] = queryResponses[reqHash]; + } + } + + function setCrossChainData( + uint64 chainSelector, + address target, + bytes memory callData, + bytes memory response + ) external { + bytes32 reqHash = keccak256(abi.encodePacked(chainSelector, target, callData)); + queryResponses[reqHash] = response; + } + + function fulfillOracleQuery(bytes calldata signedOffchainData) external payable {} + + function oracleId() external view returns (bytes32) { + return "DUMMY"; + } +} diff --git a/protocol/synthetix/contracts/modules/core/CrossChainPoolModule.sol b/protocol/synthetix/contracts/modules/core/CrossChainPoolModule.sol index 3e3feb766b..783b07e71e 100644 --- a/protocol/synthetix/contracts/modules/core/CrossChainPoolModule.sol +++ b/protocol/synthetix/contracts/modules/core/CrossChainPoolModule.sol @@ -30,11 +30,26 @@ contract CrossChainPoolModule is ICrossChainPoolModule { event OCRResponse(bytes32 indexed requestId, bytes result, bytes err); bytes32 internal constant _CREATE_CROSS_CHAIN_POOL_FEATURE_FLAG = "createCrossChainPool"; + bytes32 internal constant _SET_CROSS_CHAIN_SELECTORS = "setCrossChainPoolSelectors"; bytes32 internal constant _SET_CROSS_CHAIN_POOL_CONFIGURATION_FEATURE_FLAG = "setCrossChainPoolConfiguration"; string internal constant _CONFIG_CHAINLINK_FUNCTIONS_ADDRESS = "chainlinkFunctionsAddr"; + function setCrossChainPoolSelectors( + uint128 poolId, + bytes4 readSelector, + bytes4 writeSelector + ) external override { + FeatureFlag.ensureAccessToFeature(_SET_CROSS_CHAIN_SELECTORS); + + Pool.Data storage pool = Pool.loadExisting(poolId); + Pool.onlyPoolOwner(poolId, msg.sender); + + pool.crossChain[0].offchainReadSelector = readSelector; + pool.crossChain[0].broadcastSelector = writeSelector; + } + function createCrossChainPool( uint128 sourcePoolId, uint64 targetChainId @@ -94,6 +109,7 @@ contract CrossChainPoolModule is ICrossChainPoolModule { Pool.Data storage pool = Pool.loadExisting(poolId); Pool.onlyPoolOwner(poolId, msg.sender); + Pool.onlyCrossChainConfigured(poolId); if (newMarketConfigurations.length != pool.crossChain[0].pairedChains.length + 1) { revert ParameterError.InvalidParameter( diff --git a/protocol/synthetix/contracts/modules/core/CrossChainUpkeepModule.sol b/protocol/synthetix/contracts/modules/core/CrossChainUpkeepModule.sol index febecb3171..c3906ea656 100644 --- a/protocol/synthetix/contracts/modules/core/CrossChainUpkeepModule.sol +++ b/protocol/synthetix/contracts/modules/core/CrossChainUpkeepModule.sol @@ -43,6 +43,7 @@ contract CrossChainUpkeepModule is ICrossChainUpkeepModule { PoolCrossChainSync.Data memory syncData, int256 assignedDebt ) external override { + CrossChain.onlyCrossChain(); _receivePoolHeartbeat(poolId, syncData, assignedDebt); } @@ -57,9 +58,11 @@ contract CrossChainUpkeepModule is ICrossChainUpkeepModule { pool.setCrossChainSyncData(syncData); // assign accumulated debt + console.log("assign debt"); pool.assignDebt(assignedDebt); // make sure the markets limits are set as expect + console.log("recalculate all collaterals"); pool.recalculateAllCollaterals(); emit PoolHeartbeat(poolId, syncData); @@ -110,15 +113,82 @@ contract CrossChainUpkeepModule is ICrossChainUpkeepModule { ); } - address(this).tryCall( - abi.encodeWithSelector( - pool.crossChain[0].offchainReadSelector, - pool.crossChain[0].subscriptionId, - pool.crossChain[0].pairedChains, - calls, - 300000 - ) + bytes[] memory responses = abi.decode( + address(this).tryCall( + abi.encodeWithSelector( + pool.crossChain[0].offchainReadSelector, + pool.crossChain[0].subscriptionId, + pool.crossChain[0].pairedChains, + calls, + 300000 + ) + ), + (bytes[]) ); + + // now that we have cross chain data, lets parse the result + uint256 chainsCount = pool.crossChain[0].pairedChains.length; + + uint256[] memory liquidityAmounts = new uint256[](chainsCount); + + PoolCrossChainSync.Data memory sync; + + sync.dataTimestamp = uint64(block.timestamp); + + for (uint i = 0; i < chainsCount; i++) { + bytes[] memory chainResponses = abi.decode(responses[i], (bytes[])); + + uint256 chainLiquidity = abi.decode(chainResponses[0], (uint256)); + int256 chainCumulativeMarketDebt = abi.decode(chainResponses[1], (int256)); + int256 chainTotalDebt = abi.decode(chainResponses[2], (int256)); + uint256 chainSyncTime = abi.decode(chainResponses[3], (uint256)); + uint256 chainLastPoolConfigTime = abi.decode(chainResponses[4], (uint256)); + + liquidityAmounts[i] = chainLiquidity; + sync.liquidity += chainLiquidity.to128(); + sync.cumulativeMarketDebt += chainCumulativeMarketDebt.to128(); + sync.totalDebt += chainTotalDebt.to128(); + + sync.oldestDataTimestamp = sync.oldestDataTimestamp < chainSyncTime + ? sync.oldestDataTimestamp + : uint64(chainSyncTime); + sync.oldestPoolConfigTimestamp = sync.oldestPoolConfigTimestamp < + chainLastPoolConfigTime + ? sync.oldestPoolConfigTimestamp + : uint64(chainLastPoolConfigTime); + } + + int256 marketDebtChange = sync.cumulativeMarketDebt - + pool.crossChain[0].latestSync.cumulativeMarketDebt; + int256 remainingDebt = marketDebtChange; + + // send sync message + for (uint i = 1; i < chainsCount; i++) { + uint64[] memory targetChainIds = new uint64[](1); + targetChainIds[0] = pool.crossChain[0].pairedChains[i]; + + int256 chainAssignedDebt = (marketDebtChange * liquidityAmounts[i].toInt()) / + sync.liquidity.toInt(); + remainingDebt -= chainAssignedDebt; + + // TODO: gas limit should be based on number of markets assigned to pool + // TODO: broadcast will be paid in LINK + address(this).tryCall( + abi.encodeWithSelector( + pool.crossChain[0].broadcastSelector, + pool.crossChain[0].pairedChains, + abi.encodeWithSelector( + this._recvPoolHeartbeat.selector, + pool.crossChain[0].pairedPoolIds[targetChainIds[0]], + sync, + chainAssignedDebt + ), + 500000 + ) + ); + } + + _receivePoolHeartbeat(pool.id, sync, remainingDebt); } function _encodeCrossChainPoolCall(uint128 poolId) internal pure returns (bytes memory) { @@ -146,41 +216,4 @@ contract CrossChainUpkeepModule is ICrossChainUpkeepModule { return abi.encodeWithSelector(IMulticallModule.multicall.selector, abi.encode(calls)); } - - /** - * loads the bytes deployed to the specified address - * used to get the inline execution code for chainlink - */ - function _codeAt(address _addr) public view returns (bytes memory o_code) { - assembly { - // retrieve the size of the code, this needs assembly - let size := extcodesize(_addr) - // allocate output byte array - this could also be done without assembly - // by using o_code = new bytes(size) - o_code := mload(0x40) - // new "memory end" including padding - mstore(0x40, add(o_code, and(add(add(size, 0x20), 0x1f), not(0x1f)))) - // store length in memory - mstore(o_code, size) - // actually retrieve the code, this needs assembly - extcodecopy(_addr, add(o_code, 0x20), 0, size) - } - } - - /** - * This is basically required to send binary data to chainlink functions - */ - function _bytesToHexString(bytes memory buffer) public pure returns (string memory) { - // Fixed buffer size for hexadecimal convertion - bytes memory converted = new bytes(buffer.length * 2); - - bytes memory _base = "0123456789abcdef"; - - for (uint256 i = 0; i < buffer.length; i++) { - converted[i * 2] = _base[uint8(buffer[i]) / _base.length]; - converted[i * 2 + 1] = _base[uint8(buffer[i]) % _base.length]; - } - - return string(abi.encodePacked("0x", converted)); - } } diff --git a/protocol/synthetix/contracts/modules/core/OffchainWormholeModule.sol b/protocol/synthetix/contracts/modules/core/OffchainWormholeModule.sol index 4c2788bfa8..2df27d1f42 100644 --- a/protocol/synthetix/contracts/modules/core/OffchainWormholeModule.sol +++ b/protocol/synthetix/contracts/modules/core/OffchainWormholeModule.sol @@ -20,11 +20,12 @@ import "@synthetixio/core-contracts/contracts/errors/ParameterError.sol"; */ contract OffchainWormholeModule is IWormholeReceiver, IOffchainWormholeModule { using SetUtil for SetUtil.UintSet; + using CrossChainWormhole for CrossChainWormhole.Data; function configureWormholeCrossChain( IWormholeRelayerSend send, IWormholeRelayerDelivery recv, - IWormholeCrossChainRead read, + IWormholeERC7412Receiver read, uint64[] memory supportedNetworks, uint16[] memory selectors ) external { @@ -43,12 +44,49 @@ contract OffchainWormholeModule is IWormholeReceiver, IOffchainWormholeModule { wcc.recv = recv; for (uint i = 0; i < supportedNetworks.length; i++) { - wcc.supportedNetworks.add(supportedNetworks[i]); wcc.chainIdToSelector[supportedNetworks[i]] = selectors[i]; wcc.selectorToChainId[selectors[i]] = supportedNetworks[i]; } } + function readCrossChainWormhole( + bytes32 /* subscriptionId */, + uint64[] memory chains, + bytes memory call, + uint256 /* gasLimit */ + ) external returns (bytes[] memory responses) { + CrossChainWormhole.Data storage wcc = CrossChainWormhole.load(); + + IWormholeERC7412Receiver.CrossChainRequest[] + memory reqs = new IWormholeERC7412Receiver.CrossChainRequest[](chains.length); + + // TODO: this function simulates getting the latest data reasonably possible by choosing a timestamp just a hardcoded bit in the past. However, we could probably do better. + uint256 curTime = block.timestamp - 30; + + for (uint i = 0; i < chains.length; i++) { + reqs[i] = IWormholeERC7412Receiver.CrossChainRequest({ + chainSelector: wcc.chainIdToSelector[chains[i]], + timestamp: curTime, + target: address(this), + data: call + }); + } + + return wcc.crossChainRead.getCrossChainData(reqs, 0); + } + + function sendWormholeMessage( + uint64[] memory chainIds, + bytes memory message, + uint256 gasLimit + ) external override returns (bytes32[] memory sequenceNumbers) { + if (msg.sender != address(this)) { + revert AccessError.Unauthorized(msg.sender); + } + CrossChainWormhole.Data storage wcc = CrossChainWormhole.load(); + wcc.broadcast(chainIds, message, gasLimit); + } + function receiveWormholeMessages( bytes memory payload, bytes[] memory additionalVaas, @@ -67,7 +105,7 @@ contract OffchainWormholeModule is IWormholeReceiver, IOffchainWormholeModule { revert AccessError.Unauthorized(sourceAddr); } - if (wcc.supportedNetworks.contains(sourceChain)) { + if (wcc.selectorToChainId[sourceChain] == 0) { revert CrossChain.UnsupportedNetwork(sourceChain); } diff --git a/protocol/synthetix/contracts/storage/CrossChainWormhole.sol b/protocol/synthetix/contracts/storage/CrossChainWormhole.sol index 08297523e6..5a8490c0d2 100644 --- a/protocol/synthetix/contracts/storage/CrossChainWormhole.sol +++ b/protocol/synthetix/contracts/storage/CrossChainWormhole.sol @@ -3,7 +3,7 @@ pragma solidity >=0.8.11 <0.9.0; import {SetUtil} from "@synthetixio/core-contracts/contracts/utils/SetUtil.sol"; -import "../interfaces/external/IWormholeCrossChainRead.sol"; +import "../interfaces/external/IWormholeERC7412Receiver.sol"; import "../interfaces/external/IWormholeRelayer.sol"; import "../utils/CrossChain.sol"; @@ -17,10 +17,9 @@ library CrossChainWormhole { keccak256(abi.encode("io.synthetix.synthetix.CrossChainWormhole")); struct Data { - IWormholeCrossChainRead crossChainRead; + IWormholeERC7412Receiver crossChainRead; IWormholeRelayerSend sender; IWormholeRelayerDelivery recv; - SetUtil.UintSet supportedNetworks; mapping(uint64 => uint16) chainIdToSelector; mapping(uint16 => uint64) selectorToChainId; } @@ -32,19 +31,6 @@ library CrossChainWormhole { } } - function getCrossChainData( - uint64 chainId, - bytes memory crossChainData - ) internal returns (bytes memory) { - // wormhole is ERC7412 compliant so all we need to do is call them foir the data and the standard will do the rest - return - load().crossChainRead.getCrossChainData( - load().chainIdToSelector[chainId], - address(this), - crossChainData - ); - } - function transmit( Data storage self, uint64 chainId, diff --git a/protocol/synthetix/contracts/storage/Pool.sol b/protocol/synthetix/contracts/storage/Pool.sol index dbd01eb0b4..e4ec48ee18 100644 --- a/protocol/synthetix/contracts/storage/Pool.sol +++ b/protocol/synthetix/contracts/storage/Pool.sol @@ -13,6 +13,8 @@ import "./PoolCollateralConfiguration.sol"; import "@synthetixio/core-contracts/contracts/errors/AccessError.sol"; import "@synthetixio/core-contracts/contracts/utils/SafeCast.sol"; +import "hardhat/console.sol"; + /** * @title Aggregates collateral from multiple users in order to provide liquidity to a configurable set of markets. * @@ -51,6 +53,8 @@ library Pool { */ error PoolIsNotPrimary(uint128 poolId); + error PoolNotCrossChain(uint128 poolId); + /** * @dev Thrown when min delegation time for a market connected to the pool has not elapsed */ @@ -347,7 +351,8 @@ library Pool { int256 totalDebtD18; for (uint i = 0; i < availableCollaterals.length(); i++) { - address collateralType = availableCollaterals.valueAt(i); + console.log("accessing available collateral", i); + address collateralType = availableCollaterals.valueAt(i + 1); // Transfer the debt change from the pool into the vault. bytes32 actorId = collateralType.toBytes32(); @@ -371,6 +376,8 @@ library Pool { totalDebtD18 += debtD18; } + console.log("finished with collaterals"); + // Accumulate the change in total liquidity, from the vault, into the pool. self.totalVaultDebtsD18 = totalDebtD18.to128(); @@ -651,6 +658,12 @@ library Pool { } } + function onlyCrossChainConfigured(uint128 poolId) internal view { + if (!Pool.isCrossChainEnabled(load(poolId))) { + revert PoolNotCrossChain(poolId); + } + } + function requireMinDelegationTimeElapsed( Data storage self, uint64 lastDelegationTime diff --git a/protocol/synthetix/contracts/utils/CrossChain.sol b/protocol/synthetix/contracts/utils/CrossChain.sol index 904171c1b2..f777e9c7a8 100644 --- a/protocol/synthetix/contracts/utils/CrossChain.sol +++ b/protocol/synthetix/contracts/utils/CrossChain.sol @@ -17,14 +17,15 @@ library CrossChain { } function refundLeftoverGas(uint256 gasTokenUsed) internal returns (uint256 amountRefunded) { - amountRefunded = msg.value - gasTokenUsed; + amountRefunded = address(this).balance; + if (amountRefunded > 0) { + (bool success, bytes memory result) = msg.sender.call{value: amountRefunded}(""); - (bool success, bytes memory result) = msg.sender.call{value: amountRefunded}(""); - - if (!success) { - uint256 len = result.length; - assembly { - revert(result, len) + if (!success) { + uint256 len = result.length; + assembly { + revert(result, len) + } } } } diff --git a/protocol/synthetix/storage.dump.sol b/protocol/synthetix/storage.dump.sol index 2a6004a008..94799d4538 100644 --- a/protocol/synthetix/storage.dump.sol +++ b/protocol/synthetix/storage.dump.sol @@ -298,6 +298,16 @@ interface FunctionsBillingRegistryInterface { } } +// @custom:artifact contracts/interfaces/external/IWormholeERC7412Receiver.sol:IWormholeERC7412Receiver +interface IWormholeERC7412Receiver { + struct CrossChainRequest { + uint64 chainSelector; + uint256 timestamp; + address target; + bytes data; + } +} + // @custom:artifact contracts/interfaces/external/IWormholeRelayer.sol:IWormholeRelayerDelivery interface IWormholeRelayerDelivery { enum DeliveryStatus { @@ -543,7 +553,6 @@ library CrossChainWormhole { address crossChainRead; address sender; address recv; - SetUtil.UintSet supportedNetworks; mapping(uint64 => uint16) chainIdToSelector; mapping(uint16 => uint64) selectorToChainId; } diff --git a/protocol/synthetix/test/integration/modules/core/CrossChainPoolModule.test.ts b/protocol/synthetix/test/integration/modules/core/CrossChainPoolModule.test.ts index 6fa2270498..9813f1de96 100644 --- a/protocol/synthetix/test/integration/modules/core/CrossChainPoolModule.test.ts +++ b/protocol/synthetix/test/integration/modules/core/CrossChainPoolModule.test.ts @@ -2,34 +2,99 @@ import { ethers } from 'ethers'; import { bootstrapWithMockMarketAndPool } from '../../bootstrap'; import { verifyUsesFeatureFlag } from '../../verifications'; +import assertRevert from '@synthetixio/core-utils/utils/assertions/assert-revert'; -describe('CrossChainPoolModule', function () { - const { signers, systems } = bootstrapWithMockMarketAndPool(); +import { snapshotCheckpoint } from '@synthetixio/core-utils/utils/mocha/snapshot'; - let user1: ethers.Signer; +describe.only('CrossChainPoolModule', function () { + const { owner, signers, systems, provider, MockMarket, poolId } = + bootstrapWithMockMarketAndPool(); + + let user1: ethers.Signer, FakeWormholeSend: ethers.Signer, FakeWormholeReceive: ethers.Signer; + let FakeCcr: ethers.Contract; + //const fiftyUSD = bn(50); + //const twoHundredUSD = bn(200); before('identify signers', async () => { - [, user1] = signers(); + [, user1, FakeWormholeSend, FakeWormholeReceive] = signers(); + + const factory = await hre.ethers.getContractFactory('FakeWormholeCrossChainRead'); + + FakeCcr = await factory.connect(owner()).deploy(); + }); + + before('set wormhole settings', async () => { + await systems() + .Core.connect(owner()) + .configureWormholeCrossChain( + await FakeWormholeSend.getAddress(), + await FakeWormholeReceive.getAddress(), + FakeCcr.address, + [1, 2, 3, 4, 13370], + [10, 20, 30, 40, 13370] + ); }); + before('set test pool to use wormhole', async () => { + await systems() + .Core.connect(owner()) + .setCrossChainPoolSelectors( + poolId, + systems().Core.interface.getSighash('readCrossChainWormhole'), + systems().Core.interface.getSighash('sendCrossChainFake') + ); + }); + + const restoreWithReadyPool = snapshotCheckpoint(provider); + + // before('get some snxUSD', async () => { + // await systems() + // .Core.connect(staker()) + // .mintUsd(accountId, poolId, collateralAddress(), twoHundredUSD); + // + // await systems().Core.connect(staker()).withdraw(accountId, collateralAddress(), fiftyUSD); + // }); + describe('createCrossChainPool()', () => { + before(restoreWithReadyPool); verifyUsesFeatureFlag( () => systems().Core, 'createCrossChainPool', () => systems().Core.connect(user1).createCrossChainPool(1, ethers.constants.AddressZero) ); - it('only works for owner', async () => {}); - - it('only works if chain does not already have pool id', async () => {}); + it('only works for owner', async () => { + await assertRevert( + systems().Core.connect(user1).createCrossChainPool(1, 2), + 'Unauthorized(', + systems().Core + ); + }); describe('successful call', () => { - it('triggers cross chain call', async () => {}); + it('triggers cross chain call', async () => { + await systems().Core.connect(owner()).createCrossChainPool(1, 2); + }); + + it('does not work a second time because pool already created', async () => { + await assertRevert( + systems().Core.connect(owner()).createCrossChainPool(1, 2), + 'PoolAlreadyExists(2,', + systems().Core + ); + }); }); }); describe('_recvCreateCrossChainPool()', () => { - it('checks that its a cross chain call', async () => {}); + before(restoreWithReadyPool); + it('checks that its a cross chain call', async () => { + await assertRevert( + systems().Core._recvCreateCrossChainPool(13370, 1), + 'Unauthorized(', + systems().Core + ); + }); describe('successful call', () => { it('mark pool as created with cross chain', async () => {}); @@ -37,21 +102,50 @@ describe('CrossChainPoolModule', function () { }); describe('setCrossChainPoolConfiguration()', () => { - it('only works for owner', async () => {}); + before(restoreWithReadyPool); + + it('is only callable is pool is cross chain', async () => { + await assertRevert( + systems().Core.connect(owner()).setCrossChainPoolConfiguration(1, []), + 'PoolNotCrossChain(', + systems().Core + ); + }); - it('is only callable is pool is cross chain', async () => {}); + describe('when cross chain pool', async () => { + before('make the pool cross chain', async () => { + await systems().Core.connect(owner()).createCrossChainPool(1, 2); + }); + it('only works for owner', async () => { + await assertRevert( + systems().Core.connect(user1).setCrossChainPoolConfiguration(1, []), + 'Unauthorized(', + systems().Core + ); + }); - describe('successful call', () => { - it('sets local decreasing market capacities', async () => {}); + describe('successful call', () => { + it('sets local decreasing market capacities', async () => {}); - describe('finish sync', () => { - it('sets increasing market capacities', async () => {}); + describe('finish sync', () => { + it('sets increasing market capacities', async () => {}); + }); }); }); }); describe('_recvSetCrossChainPoolConfiguration', () => { - it('checks cross chain', async () => {}); + before(restoreWithReadyPool); + before('make the pool cross chain', async () => { + await systems().Core.connect(owner()).createCrossChainPool(1, 2); + }); + it('checks cross chain', async () => { + await assertRevert( + systems().Core._recvSetCrossChainPoolConfiguration(1, [], 0, 0), + 'Unauthorized(', + systems().Core + ); + }); describe('successful call', () => { it('sets local decreasing market capacities', async () => {}); @@ -61,34 +155,4 @@ describe('CrossChainPoolModule', function () { }); }); }); - - describe('_recvPoolHeartbeat()', () => { - it('checks cross chain', async () => {}); - - describe('successful call', () => { - it('sets pool sync info', async () => {}); - - it('assigns specified debt to vaults', async () => {}); - - it('rebalances vault collaterals', async () => {}); - - it('emits event', async () => {}); - }); - }); - - describe('handleOracleFulfillment()', () => { - it('checks that its chainlink functions only', async () => {}); - - describe('successful call', async () => { - it('triggers pool heartbeats', async () => {}); - }); - }); - - describe('performUpkeep()', () => { - it('checks that upkeep is necessary', async () => {}); - - describe('successful call', async () => { - it('triggers chainlink functions', async () => {}); - }); - }); }); diff --git a/protocol/synthetix/test/integration/modules/core/CrossChainUpkeepModule.test.ts b/protocol/synthetix/test/integration/modules/core/CrossChainUpkeepModule.test.ts index 588ebdac70..4c6818dd45 100644 --- a/protocol/synthetix/test/integration/modules/core/CrossChainUpkeepModule.test.ts +++ b/protocol/synthetix/test/integration/modules/core/CrossChainUpkeepModule.test.ts @@ -2,9 +2,11 @@ import { ethers } from 'ethers'; import { bootstrapWithMockMarketAndPool } from '../../bootstrap'; import assertRevert from '@synthetixio/core-utils/utils/assertions/assert-revert'; +import assertEvent from '@synthetixio/core-utils/utils/assertions/assert-event'; +import assertBn from '@synthetixio/core-utils/utils/assertions/assert-bignumber'; describe('CrossChainUpkeepModule', function () { - const { signers, systems } = bootstrapWithMockMarketAndPool(); + const { signers, systems, collateralAddress, marketId, owner } = bootstrapWithMockMarketAndPool(); let user1: ethers.Signer; @@ -12,45 +14,61 @@ describe('CrossChainUpkeepModule', function () { [, user1] = signers(); }); - describe('_recvPoolHeartbeat()', () => { - it('checks cross chain', async () => {}); - - describe('successful call', () => { - it('sets pool sync info', async () => {}); - - it('assigns specified debt to vaults', async () => {}); - - it('rebalances vault collaterals', async () => {}); - - it('emits event', async () => {}); - }); - }); - - describe('handleOracleFulfillment()', () => { - const requestId = ethers.utils.formatBytes32String('woot'); - - before('initiate fake functions call', async () => {}); - - it('checks that its chainlink functions only', async () => { + describe.only('_recvPoolHeartbeat()', () => { + it('checks cross chain', async () => { await assertRevert( - systems().Core.handleOracleFulfillment(requestId, '0x', '0x'), - `Unauthorized(${await owner.getAddress()})`, + systems().Core._recvPoolHeartbeat( + 1, + { + liquidity: 0, + cumulativeMarketDebt: 0, + totalDebt: 0, + dataTimestamp: 0, + oldestDataTimestamp: 0, + oldestPoolConfigTimestamp: 0, + }, + 1 + ), + 'Unauthorized(', systems().Core ); }); - describe('chainlink call failure', async () => { - it('does nothing', async () => { - await systems().Core.connect(user1).handleOracleFulfillment(requestId, '0x', '0xfoobar'); + describe('successful call', () => { + let txn: ethers.providers.TransactionResponse; + before('do the call', async () => { + const call = systems().Core.interface.encodeFunctionData('_recvPoolHeartbeat', [ + 1, + { + liquidity: 1, + cumulativeMarketDebt: 2, + totalDebt: 3, + dataTimestamp: 4, + oldestDataTimestamp: 5, + oldestPoolConfigTimestamp: 6, + }, + 1, + ]); + + txn = await systems().Core.callSelf(call); }); - }); + it('sets pool sync info', async () => {}); - describe('successful call', async () => { - before('exec', async () => { - await systems().Core.connect(user1).handleOracleFulfillment(requestId, '0xfoobar', '0x'); + it('assigns specified debt to vaults', async () => { + assertBn.equal(await systems().Core.callStatic.getVaultDebt(1, collateralAddress()), 1); + }); + + it('rebalances vault collaterals', async () => { + // check connected market's credit capacity, should have gone down. If so, that means that rebalance occured + assertBn.equal( + await systems().Core.getWithdrawableMarketUsd(marketId()), + ethers.utils.parseEther('1000') + ); }); - it('triggers pool heartbeats', async () => {}); + it('emits event', async () => { + await assertEvent(txn, `PoolHeartbeat(1`, systems().Core); + }); }); }); diff --git a/protocol/synthetix/test/integration/modules/core/OffchainWormholeModule.test.ts b/protocol/synthetix/test/integration/modules/core/OffchainWormholeModule.test.ts index f4cdf3921d..58e95db265 100644 --- a/protocol/synthetix/test/integration/modules/core/OffchainWormholeModule.test.ts +++ b/protocol/synthetix/test/integration/modules/core/OffchainWormholeModule.test.ts @@ -1,40 +1,38 @@ +import assert from 'assert/strict'; import assertBn from '@synthetixio/core-utils/utils/assertions/assert-bignumber'; import assertEvent from '@synthetixio/core-utils/utils/assertions/assert-event'; import assertRevert from '@synthetixio/core-utils/utils/assertions/assert-revert'; import { ethers } from 'ethers'; -import { bn, bootstrapWithStakedPool } from '../../bootstrap'; +import { bn, bootstrapWithMockMarketAndPool } from '../../bootstrap'; -describe('CcipReceiverModule', function () { - const { owner, signers, systems, staker, accountId, poolId, collateralAddress } = - bootstrapWithStakedPool(); +describe.only('OffchainWormholeModule', function () { + const { owner, signers, systems, staker, accountId, poolId, collateralAddress, MockMarket } = + bootstrapWithMockMarketAndPool(); - let FakeCcip: ethers.Signer; + let FakeWormholeSend: ethers.Signer, FakeWormholeReceive: ethers.Signer; + let FakeCcr: ethers.Contract; const fiftyUSD = bn(50); const twoHundredUSD = bn(200); - let proxyBalanceBefore: ethers.BigNumber, stakerBalanceBefore: ethers.BigNumber; + before('identify signers', async () => { + [FakeWormholeSend, FakeWormholeReceive] = signers(); - const abiCoder = new ethers.utils.AbiCoder(); + const factory = await hre.ethers.getContractFactory('FakeWormholeCrossChainRead'); - before('identify signers', async () => { - [FakeCcip] = signers(); + FakeCcr = await factory.connect(owner()).deploy(); }); - before('set ccip settings', async () => { + before('set wormhole settings', async () => { await systems() .Core.connect(owner()) - .configureChainlinkCrossChain( - await FakeCcip.getAddress(), - ethers.constants.AddressZero, - ethers.constants.AddressZero, - [], - [] + .configureWormholeCrossChain( + await FakeWormholeSend.getAddress(), + await FakeWormholeReceive.getAddress(), + FakeCcr.address, + [1, 2, 3, 4, 13370], + [10, 20, 30, 40, 13370] ); - - await systems() - .Core.connect(owner()) - .setSupportedCrossChainNetworks([1234, 2192], [1234, 2192]); }); before('get some snxUSD', async () => { @@ -45,133 +43,89 @@ describe('CcipReceiverModule', function () { await systems().Core.connect(staker()).withdraw(accountId, collateralAddress(), fiftyUSD); }); - before('record balances', async () => { - stakerBalanceBefore = await systems() - .USD.connect(staker()) - .balanceOf(await staker().getAddress()); - proxyBalanceBefore = await systems().USD.connect(staker()).balanceOf(systems().Core.address); - }); + describe('readCrossChainWormhole()', () => { + before('set cross chain data', async () => { + await FakeCcr.setCrossChainData(10, systems().Core.address, '0x12345678', '0x01'); + await FakeCcr.setCrossChainData(20, systems().Core.address, '0x12345678', '0x0202'); + await FakeCcr.setCrossChainData(30, systems().Core.address, '0x12345678', '0x03'); + await FakeCcr.setCrossChainData(40, systems().Core.address, '0x12345678', '0x04'); + await FakeCcr.setCrossChainData(13370, systems().Core.address, '0x12345678', '0x1337'); + }); - describe('ccipReceive()', () => { - it('fails if caller is not CCIP router', async () => { - await assertRevert( - systems() - .Core.connect(staker()) - .ccipReceive({ - messageId: ethers.constants.HashZero, - sourceChainSelector: 1234, - sender: ethers.utils.defaultAbiCoder.encode(['address'], [systems().Core.address]), - data: '0x', - tokenAmounts: [], - }), - `NotCcipRouter("${await staker().getAddress()}")`, - systems().Core + it('returns whatever cross chain data from the contract', async () => { + const responses = await systems().Core.callStatic.readCrossChainWormhole( + ethers.constants.HashZero, // unused + [1, 2, 3, 4, 13370], + '0x12345678', + 0 // unused ); + + assert(responses.length == 5); + assert(responses[0] == '0x01'); + assert(responses[1] == '0x0202'); + assert(responses[2] == '0x03'); + assert(responses[3] == '0x04'); + assert(responses[4] == '0x1337'); }); + }); - it('fails if chain is not supported', async () => { + describe('sendWormholeMessage()', async () => { + it('only callable by the core system', async () => { await assertRevert( - systems() - .Core.connect(FakeCcip) - .ccipReceive({ - messageId: ethers.constants.HashZero, - sourceChainSelector: 1111, - sender: ethers.utils.defaultAbiCoder.encode(['address'], [systems().Core.address]), - data: '0x', - tokenAmounts: [], - }), - `UnsupportedNetwork("0")`, + systems().Core.sendWormholeMessage([1, 2, 3], '0x12345678', 1234567), + 'Unauthorized(', systems().Core ); }); + }); - it('fails if message sender on other chain is not self', async () => { + describe('receiveWormholeMessages()', async () => { + it('fails if not wormhole sender', async () => { await assertRevert( systems() - .Core.connect(FakeCcip) - .ccipReceive({ - messageId: ethers.constants.HashZero, - sourceChainSelector: 1234, - sender: ethers.utils.defaultAbiCoder.encode(['address'], [await FakeCcip.getAddress()]), - data: '0x', - tokenAmounts: [], - }), + .Core.connect(FakeWormholeSend) + .receiveWormholeMessages( + systems().Core.interface.encodeFunctionData('createAccount()'), + [], + ethers.utils.defaultAbiCoder.encode(['address'], [systems().Core.address]), + 10, + ethers.constants.HashZero + ), 'Unauthorized(', systems().Core ); }); - it('fails if token amount data is invalid', async () => { + it('fails if the cross chain sender is not self', async () => { await assertRevert( systems() - .Core.connect(FakeCcip) - .ccipReceive({ - messageId: ethers.constants.HashZero, - sourceChainSelector: 1234, - sender: ethers.utils.defaultAbiCoder.encode(['address'], [systems().Core.address]), - data: abiCoder.encode(['address'], [await staker().getAddress()]), - tokenAmounts: [ - { - token: systems().USD.address, - amount: fiftyUSD, - }, - { - token: systems().USD.address, - amount: fiftyUSD, - }, - ], - }), - 'InvalidMessage()', + .Core.connect(FakeWormholeReceive) + .receiveWormholeMessages( + systems().Core.interface.encodeFunctionData('createAccount()'), + [], + ethers.utils.defaultAbiCoder.encode(['address'], [await FakeWormholeSend.getAddress()]), + 10, + ethers.constants.HashZero + ), + 'Unauthorized(', systems().Core ); }); - describe('receives a token amount message', () => { - let receivedTxn: ethers.providers.TransactionResponse; - let receipt: ethers.providers.TransactionReceipt; - - before('calls ccip receive', async () => { - receivedTxn = await systems() - .Core.connect(FakeCcip) - .ccipReceive({ - messageId: ethers.constants.HashZero, - sourceChainSelector: 1234, - sender: abiCoder.encode(['address'], [systems().Core.address]), - data: abiCoder.encode(['address'], [await staker().getAddress()]), - tokenAmounts: [ - { - token: systems().USD.address, - amount: fiftyUSD, - }, - ], - }); - - receipt = await (receivedTxn as ethers.providers.TransactionResponse).wait(); - }); - - it('should transfer the snxUSD from the core proxy', async () => { - const proxyBalanceAfter = await systems() - .USD.connect(owner()) - .balanceOf(systems().Core.address); - assertBn.equal(proxyBalanceAfter, proxyBalanceBefore.sub(fiftyUSD)); - }); - - it('should increase the stakers balance by the expected amount', async () => { - const stakerBalanceAfter = await systems() - .USD.connect(staker()) - .balanceOf(await staker().getAddress()); - assertBn.equal(stakerBalanceAfter, stakerBalanceBefore.add(fiftyUSD)); - }); - - describe('emits expected events', () => { - it('emits a Transfer event', async () => { - await assertEvent( - receipt, - `Transfer("${systems().Core.address}", "${await staker().getAddress()}", ${fiftyUSD})`, - systems().USD - ); - }); - }); + it('can call a function from cross chain receiver', async () => { + await assertEvent( + await systems() + .Core.connect(FakeWormholeReceive) + .receiveWormholeMessages( + systems().Core.interface.encodeFunctionData('registerMarket', [MockMarket().address]), + [], + ethers.utils.defaultAbiCoder.encode(['address'], [systems().Core.address]), + 10, + ethers.constants.HashZero + ), + 'MarketRegistered(', + systems().Core + ); }); }); });