diff --git a/.gitmodules b/.gitmodules index d84f218..efa1ba2 100644 --- a/.gitmodules +++ b/.gitmodules @@ -16,3 +16,9 @@ [submodule "lib/ve-beacon"] path = lib/ve-beacon url = https://github.com/timeless-fi/ve-beacon +[submodule "lib/devtools"] + path = lib/devtools + url = https://github.com/LayerZero-Labs/devtools +[submodule "lib/layerzero-v2"] + path = lib/layerzero-v2 + url = https://github.com/LayerZero-Labs/layerzero-v2 diff --git a/foundry.toml b/foundry.toml index 883a4a0..cddbe5a 100644 --- a/foundry.toml +++ b/foundry.toml @@ -9,7 +9,12 @@ remappings = [ # Foundry fails to compute the correct path. By adding it to the root # project's remappings we can work around the issue. If it's fixed in the future we should update the repo. 'openzeppelin-contracts-upgradeable/=lib/popcorn/lib/openzeppelin-contracts-upgradeable/contracts', + '@layerzerolabs/oft-evm/=lib/devtools/packages/oft-evm/', + '@layerzerolabs/oapp-evm/=lib/devtools/packages/oapp-evm/', + '@layerzerolabs/lz-evm-protocol-v2/=lib/layerzero-v2/packages/layerzero-v2/evm/protocol', + '@popcorn/=lib/popcorn/' ] + verbosity = 1 evm_version = "shanghai" diff --git a/lib/devtools b/lib/devtools new file mode 160000 index 0000000..108f4d6 --- /dev/null +++ b/lib/devtools @@ -0,0 +1 @@ +Subproject commit 108f4d698d7831c00ebffea972123aabd2a231a1 diff --git a/lib/layerzero-v2 b/lib/layerzero-v2 new file mode 160000 index 0000000..34321ac --- /dev/null +++ b/lib/layerzero-v2 @@ -0,0 +1 @@ +Subproject commit 34321ac15e47e0dafd25d66659e2f3d1b9b6db8f diff --git a/script/DeployOVCX.s.sol b/script/DeployOVCX.s.sol new file mode 100644 index 0000000..541325a --- /dev/null +++ b/script/DeployOVCX.s.sol @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.25; + +import {Script, console} from "forge-std/Script.sol"; +import {oVCXSender} from "../src/oVCX_Sender.sol"; +import {oVCXRecipient} from "../src/oVCX_Recipient.sol"; + +// 1 - DeployOVCXBridge on root chain +// 2 - WhitelistDestination on root chain +// 3 - DeployOVCXL2 on recipient chain +// 4 - SetReceiverPeer on root chain +// 5 - WhitelistReceiver on recipient chain + +// ------------------------------- ROOT LZ APP ------------------------------- // + +// deploy LZ oVCX Root bridge +contract DeployOVCXBridge is Script { + function run() public returns (oVCXSender bridge) { + vm.startBroadcast(); + console.log("msg.sender:", msg.sender); + + // oVCX address + address tokenToBridge = address( + 0xb1D4538B4571d411F07960EF2838Ce337FE1E80E + ); // modify + + // see https://docs.layerzero.network/v2/developers/evm/technical-reference/deployed-contracts + address lzEndpoint = address( + 0x6EDCE65403992e310A62460808c4b910D972f10f + ); // modify + + // deploy + address owner = msg.sender; + bridge = new oVCXSender(tokenToBridge, lzEndpoint, owner); + + vm.stopBroadcast(); + } +} + +// to whitelist new address that can receive crosschain token from bridge +contract WhitelistDestination is Script { + function run() public { + vm.startBroadcast(); + console.log("msg.sender:", msg.sender); + + // initialize bridge + oVCXSender bridge = oVCXSender( + address(0x3C54b43FEC8f796911ef7e74A375aE9d8E5c2f51) + ); // modify + + // whitelist receiver and LZ endpoint id + address receiverToWhitelist = address(0); // modify + uint32 destinationEndpointId = 40232; // modify + + bridge.addGauge(receiverToWhitelist, destinationEndpointId); + + vm.stopBroadcast(); + } +} + +// to register a new chain: set peer with LZ receiver contract +contract SetReceiverPeer is Script { + function run() public { + vm.startBroadcast(); + console.log("msg.sender:", msg.sender); + + // initialize bridge + oVCXSender bridge = oVCXSender( + address(0x3C54b43FEC8f796911ef7e74A375aE9d8E5c2f51) + ); // modify + + // set LZ peer to receiver app + uint32 destinationEndpointId = 40232; // LZ receiver endpoint ID + address destinationLZPeer = address( + 0x8f3C2301238e3e03C0e402aB078F9aBcfF422ADD + ); // LZ receiver app + + bridge.setPeer( + destinationEndpointId, + addressToBytes32(destinationLZPeer) + ); + + vm.stopBroadcast(); + } + + function addressToBytes32(address _addr) public pure returns (bytes32) { + return bytes32(uint256(uint160(_addr))); + } +} + +// ------------------------------- RECEIVER LZ APP ------------------------------- // + +// deploys oVCX on L2 and sets peer with root oVCX bridge +contract DeployOVCXL2 is Script { + function run() public returns (oVCXRecipient oVCX) { + uint32 receiverEndpointID = 40232; // LZ endpointID of the receiver being deployed + uint32 rootEndpointID = 40231; // LZ endpointID of the root bridge + + vm.startBroadcast(); + console.log("msg.sender:", msg.sender); + + // see https://docs.layerzero.network/v2/developers/evm/technical-reference/deployed-contracts + address lzEndpoint = address( + 0x6EDCE65403992e310A62460808c4b910D972f10f + ); // modify + + // root bridge address + address bridge = address(0x3C54b43FEC8f796911ef7e74A375aE9d8E5c2f51); // modify + + // deploy L2 oVCX - sets msg.sender as owner + oVCX = new oVCXRecipient("VCX call option token", "oVCX", lzEndpoint); + + // set peer with root bridge + oVCX.setPeer(rootEndpointID, addressToBytes32(bridge)); + + vm.stopBroadcast(); + } + + function addressToBytes32(address _addr) public pure returns (bytes32) { + return bytes32(uint256(uint160(_addr))); + } +} + +// to whitelist new address that can receive crosschain token from bridge +contract WhitelistReceiver is Script { + function run() public { + vm.startBroadcast(); + console.log("msg.sender:", msg.sender); + + // initialize recipient contract + oVCXRecipient receiverBridge = oVCXRecipient( + address(0x8f3C2301238e3e03C0e402aB078F9aBcfF422ADD) + ); // modify + + // whitelist receiver and LZ endpoint id + address receiverToWhitelist = address(0); // modify + uint32 sourceEndpointId = 40231; // modify + + receiverBridge.addGauge(receiverToWhitelist, sourceEndpointId); + + vm.stopBroadcast(); + } +} diff --git a/src/interfaces/IBridger.sol b/src/interfaces/IBridger.sol index 8e61289..09fff63 100644 --- a/src/interfaces/IBridger.sol +++ b/src/interfaces/IBridger.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.0; interface IBridger { + function check(address toCheck) external view returns (bool); function cost() external view returns (uint256); function bridge(address token, address dest, uint256 amount) external payable; } diff --git a/src/oVCX_Recipient.sol b/src/oVCX_Recipient.sol new file mode 100644 index 0000000..89fa33b --- /dev/null +++ b/src/oVCX_Recipient.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.22; + +import {OFT} from "@layerzerolabs/oft-evm/contracts/OFT.sol"; + +// oVCX on L2: ERC20 + Layer Zero receiver +contract oVCXRecipient is OFT { + mapping(address => uint32) public supportedGauges; + + // todo check crosschain origin to be a supported gauge? + constructor( + string memory _name, + string memory _symbol, + address _lzEndpoint + ) OFT(_name, _symbol, _lzEndpoint, msg.sender) {} + + // set a layer zero endpoint ID for bridging gauge rewards + function addGauge(address gauge, uint32 srcEid) external onlyOwner { + supportedGauges[gauge] = srcEid; + } + + function _credit( + address _to, + uint256 _amountLD, + uint32 _srcEid + ) internal override returns (uint256 amountReceivedLD) { + require(supportedGauges[_to] == _srcEid, "Invalid sender"); + + return super._credit(_to, _amountLD, _srcEid); + } +} diff --git a/src/oVCX_Sender.sol b/src/oVCX_Sender.sol new file mode 100644 index 0000000..ac8a9d7 --- /dev/null +++ b/src/oVCX_Sender.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.22; + +import {OFTAdapter} from "@layerzerolabs/oft-evm/contracts/OFTAdapter.sol"; +import {SendParam, MessagingFee, OFTMsgCodec} from "@layerzerolabs/oft-evm/contracts/OFTCore.sol"; +import {IBridger} from "./interfaces/IBridger.sol"; + +// Bridger contract to lock and bridge oVCX emissions to any chain +// implements IBridger iface to be compatible with gauges +contract oVCXSender is OFTAdapter, IBridger { + mapping(address => uint32) public supportedGauges; + + address public defaultGauge; // used to estimate a "default" bridging cost + + constructor( + address _token, + address _lzEndpoint, + address _owner + ) OFTAdapter(_token, _lzEndpoint, _owner) {} + + // whitelist a destination address (gauge) + function addGauge(address gauge, uint32 destEid) external onlyOwner { + if(defaultGauge == address(0)) + defaultGauge = gauge; + + supportedGauges[gauge] = destEid; + } + + // update default gauge + function setDefaultGauge(address gauge, uint32 destEid) external onlyOwner { + defaultGauge = gauge; + + supportedGauges[gauge] = destEid; + } + + // called by RootGaugeFactory with msg.sender originating the tx + // implements IBridger iface - check the originator sender + function check(address) external view returns (bool) { + return true; + } + + // called by RootGaugeFactory to estimate msg.value necessary for bridging + // implements IBridger iface + function cost() external view returns (uint256) { + return _quoteSend(defaultGauge, 1e18); + } + + // get msg.value necessary to bridge to a speficic chain + function quote( + address destinationGauge, + uint256 amount + ) external view returns (uint256 value) { + return _quoteSend(destinationGauge, amount); + } + + // bridges to the destination address via LZ + function bridge(address, address dest, uint256 amount) external payable { + uint32 destEid = supportedGauges[dest]; + require(destEid != 0, "Add gauge"); + + // lock tokens in the adapter + (uint256 amountSentLD, ) = _debit(msg.sender, amount, amount, destEid); + + // build LZ message and options + (bytes memory message, bytes memory options) = _getMessageAndOptions( + _addressToBytes32(dest), + amountSentLD + ); + + // Sends the message to the LayerZero endpoint + _lzSend( + destEid, + message, + options, + MessagingFee(msg.value, 0), + msg.sender + ); + } + + // builds crosschain message and ask for a quote to LZ endpoint + function _quoteSend( + address receiver, + uint256 amount + ) internal view returns (uint256) { + uint32 destinationEndpointId = supportedGauges[receiver]; + require(destinationEndpointId != 0, "Add gauge"); + + SendParam memory sendParams = SendParam( + destinationEndpointId, + _addressToBytes32(receiver), + amount, + amount, + hex"", + hex"", + hex"" + ); + + // simulate a debit call + (uint256 amountSendLD, ) = _debitView( + sendParams.amountLD, + sendParams.minAmountLD, + sendParams.dstEid + ); + + // build LZ message and options + (bytes memory message, bytes memory options) = _getMessageAndOptions( + sendParams.to, + amountSendLD + ); + + // quote LZ endpoint for a fee value + MessagingFee memory fee = _quote( + sendParams.dstEid, + message, + options, + false + ); + + return fee.nativeFee; + } + + function _getMessageAndOptions( + bytes32 destAddr, + uint256 amountSentLD + ) internal view returns (bytes memory m, bytes memory o) { + (m, ) = OFTMsgCodec.encode(destAddr, _toSD(amountSentLD), hex""); + + // hardcoded options for a vanilla OFT bridge + o = hex"0003010011010000000000000000000000000000ea60"; + } + + function _addressToBytes32(address _addr) internal pure returns (bytes32) { + return bytes32(uint256(uint160(_addr))); + } +}