From b588b84fc1e3c8a0ba28d52aeb27dfa5f85957f9 Mon Sep 17 00:00:00 2001 From: xiaoch05 Date: Tue, 12 Sep 2023 17:01:27 +0800 Subject: [PATCH] test script --- helix-contract/address/ln-dev.json | 4 +- .../contracts/ln/LnDefaultBridge.sol | 2 +- .../contracts/ln/LnOppositeBridge.sol | 4 +- .../ln/base/LnOppositeBridgeTarget.sol | 21 +- helix-contract/deploy/deploy_ln_configure.js | 3 +- helix-contract/deploy/deploy_ln_logic.js | 22 +- helix-contract/deploy/deploy_ln_test.js | 451 ++++++++ helix-contract/deploy/flatten-ln.sh | 1 + .../flatten/lnv2/Eth2LineaReceiveService.sol | 2 +- .../flatten/lnv2/Eth2LineaSendService.sol | 2 +- .../flatten/lnv2/LayerZeroMessager.sol | 334 ++++++ .../flatten/lnv2/LnDefaultBridge.sol | 348 +++--- .../flatten/lnv2/LnOppositeBridge.sol | 1019 +++++++++-------- 13 files changed, 1510 insertions(+), 703 deletions(-) create mode 100644 helix-contract/deploy/deploy_ln_test.js create mode 100644 helix-contract/flatten/lnv2/LayerZeroMessager.sol diff --git a/helix-contract/address/ln-dev.json b/helix-contract/address/ln-dev.json index 3b1b88a8..0095d2bf 100644 --- a/helix-contract/address/ln-dev.json +++ b/helix-contract/address/ln-dev.json @@ -18,8 +18,8 @@ } }, "ProxyAdmin": "0xE3979fFa68BBa1F53c6F502c8F5788B370d28730", - "LnDefaultBridgeLogic": "0x43ae847d170e8AB26901a80b474d356Aaa30CEE1", - "LnOppositeBridgeLogic": "0x5ae6AF7b7fb6c869219289ba2570C6D4eF20Be2a", + "LnDefaultBridgeLogic": "0x230Bd5EeECaB61CcBdcd0bCeC4F2da83e6d56B8F", + "LnOppositeBridgeLogic": "0xcfeb6AE6099DE8655a78e099571D25a21DeAD6d3", "LnDefaultBridgeProxy": "0x54cc9716905ba8ebdD01E6364125cA338Cd0054E", "LnOppositeBridgeProxy": "0x79e6f452f1e491a7aF0382FA0a6EF9368691960D" } diff --git a/helix-contract/contracts/ln/LnDefaultBridge.sol b/helix-contract/contracts/ln/LnDefaultBridge.sol index fc9134b4..85ff84bf 100644 --- a/helix-contract/contracts/ln/LnDefaultBridge.sol +++ b/helix-contract/contracts/ln/LnDefaultBridge.sol @@ -61,7 +61,7 @@ contract LnDefaultBridge is Initializable, LnAccessController, LnDefaultBridgeSo function _sendMessageToTarget(uint256 _remoteChainId, bytes memory _payload, bytes memory _extParams) internal override { address sendService = messagers[_remoteChainId].sendService; require(sendService != address(0), "invalid messager"); - ILowLevelMessageSender(sendService).sendMessage(_remoteChainId, _payload, _extParams); + ILowLevelMessageSender(sendService).sendMessage{value: msg.value}(_remoteChainId, _payload, _extParams); } function _verifyRemote(uint256 _remoteChainId) whenNotPaused internal view override { diff --git a/helix-contract/contracts/ln/LnOppositeBridge.sol b/helix-contract/contracts/ln/LnOppositeBridge.sol index 6b9b6e33..ef430bbc 100644 --- a/helix-contract/contracts/ln/LnOppositeBridge.sol +++ b/helix-contract/contracts/ln/LnOppositeBridge.sol @@ -12,7 +12,7 @@ contract LnOppositeBridge is Initializable, LnAccessController, LnOppositeBridge address sendService; address receiveService; } - mapping(uint256=>MessagerService) messagers; + mapping(uint256=>MessagerService) public messagers; receive() external payable {} @@ -58,7 +58,7 @@ contract LnOppositeBridge is Initializable, LnAccessController, LnOppositeBridge function _sendMessageToTarget(uint256 _remoteChainId, bytes memory _payload, bytes memory _extParams) internal override { address sendService = messagers[_remoteChainId].sendService; require(sendService != address(0), "invalid messager"); - ILowLevelMessageSender(sendService).sendMessage(_remoteChainId, _payload, _extParams); + ILowLevelMessageSender(sendService).sendMessage{value: msg.value}(_remoteChainId, _payload, _extParams); } function _verifyRemote(uint256 _remoteChainId) whenNotPaused internal view override { diff --git a/helix-contract/contracts/ln/base/LnOppositeBridgeTarget.sol b/helix-contract/contracts/ln/base/LnOppositeBridgeTarget.sol index 6759f977..2cabc71f 100644 --- a/helix-contract/contracts/ln/base/LnOppositeBridgeTarget.sol +++ b/helix-contract/contracts/ln/base/LnOppositeBridgeTarget.sol @@ -119,7 +119,7 @@ contract LnOppositeBridgeTarget { slashInfos[_expectedTransferId] = SlashInfo(_params.provider, _params.sourceToken, _params.targetToken, msg.sender, _params.timestamp); // Do not slash `transferId` in source chain unless `latestSlashTransferId` has been slashed - message = _encodeSlashCall( + message = encodeSlashCall( fillTransfers[_expectedTransferId], _expectedTransferId, _params.timestamp, @@ -139,7 +139,7 @@ contract LnOppositeBridgeTarget { // transfer must be slashed SlashInfo memory slashInfo = slashInfos[_transferId]; require(slashInfo.slasher != address(0), "slasher not exist"); - message = _encodeSlashCall( + message = encodeSlashCall( latestSlashTransferId, _transferId, slashInfo.timestamp, @@ -150,7 +150,7 @@ contract LnOppositeBridgeTarget { ); } - function _encodeSlashCall( + function encodeSlashCall( bytes32 _latestSlashTransferId, bytes32 _transferId, uint256 _timestamp, @@ -158,7 +158,7 @@ contract LnOppositeBridgeTarget { address _targetToken, address _provider, address _slasher - ) internal view returns(bytes memory) { + ) public view returns(bytes memory) { return abi.encodeWithSelector( ILnOppositeBridgeSource.slash.selector, _latestSlashTransferId, @@ -198,14 +198,17 @@ contract LnOppositeBridgeTarget { _sendMessageToTarget(remoteChainId, retryCallMessage, _extParams); } - function _requestWithdrawMargin( + function encodeWithdrawMargin( bytes32 _lastTransferId, address _sourceToken, address _targetToken, uint112 _amount - ) internal view returns(bytes memory message) { - bytes32 latestSlashTransferId = fillTransfers[_lastTransferId]; - require(latestSlashTransferId != bytes32(0), "invalid last transfer"); + ) public view returns(bytes memory message) { + bytes32 latestSlashTransferId = LnBridgeHelper.INIT_SLASH_TRANSFER_ID; + if (_lastTransferId != bytes32(0)) { + latestSlashTransferId = fillTransfers[_lastTransferId]; + require(latestSlashTransferId != bytes32(0), "invalid last transfer"); + } return abi.encodeWithSelector( ILnOppositeBridgeSource.withdrawMargin.selector, @@ -227,7 +230,7 @@ contract LnOppositeBridgeTarget { uint112 _amount, bytes memory _extParams ) payable external { - bytes memory withdrawCallMessage = _requestWithdrawMargin( + bytes memory withdrawCallMessage = encodeWithdrawMargin( _lastTransferId, _sourceToken, _targetToken, diff --git a/helix-contract/deploy/deploy_ln_configure.js b/helix-contract/deploy/deploy_ln_configure.js index ed9de113..db1a95d8 100644 --- a/helix-contract/deploy/deploy_ln_configure.js +++ b/helix-contract/deploy/deploy_ln_configure.js @@ -313,7 +313,8 @@ async function main() { //await connectAll(arbWallet, lineaWallet, goerliWallet, mantleWallet); //await registerAllToken(arbWallet, lineaWallet, goerliWallet, mantleWallet); //await mintAndApproveAll(arbWallet, lineaWallet, goerliWallet, mantleWallet); - await registerAllRelayer(arbWallet, lineaWallet, goerliWallet, mantleWallet); + await registerRelayer("LnDefaultBridge", goerliWallet, lineaWallet, goerliNetwork, lineaNetwork, "usdt"); + //await registerAllRelayer(arbWallet, lineaWallet, goerliWallet, mantleWallet); } main() diff --git a/helix-contract/deploy/deploy_ln_logic.js b/helix-contract/deploy/deploy_ln_logic.js index cfe84f12..b05d4d35 100644 --- a/helix-contract/deploy/deploy_ln_logic.js +++ b/helix-contract/deploy/deploy_ln_logic.js @@ -37,6 +37,7 @@ async function deployLnDefaultBridge(wallet, deployerAddress, salt) { const bytecode = Create2.getDeployedBytecode(bridgeContract, [], []); const address = await Create2.deploy(deployerAddress, wallet, bytecode, salt); console.log("finish to deploy ln default bridge logic, address: ", address); + return address; } async function deployLnOppositeBridge(wallet, deployerAddress, salt) { @@ -44,17 +45,30 @@ async function deployLnOppositeBridge(wallet, deployerAddress, salt) { const bytecode = Create2.getDeployedBytecode(bridgeContract, [], []); const address = await Create2.deploy(deployerAddress, wallet, bytecode, salt); console.log("finish to deploy ln opposite bridge logic, address: ", address); + return address; } // 2. deploy mapping token factory async function main() { + const networks = [goerliNetwork, mantleNetwork, arbitrumNetwork, lineaNetwork]; //const network = goerliNetwork; //const network = mantleNetwork; //const network = arbitrumNetwork; - const network = lineaNetwork; - const w = wallet(network.url); - //await deployLnDefaultBridge(w, network.deployer, "ln-default-logic-v1.0.1"); - await deployLnOppositeBridge(w, network.deployer, "ln-opposite-logic-v1.0.2"); + //const network = lineaNetwork; + //const w = wallet(network.url); + //await deployLnDefaultBridge(w, network.deployer, "ln-default-logic-v1.0.3"); + //await deployLnOppositeBridge(w, network.deployer, "ln-opposite-logic-v1.0.2"); + for (const network of networks) { + // deploy logic bridge + const w = wallet(network.url); + //const logicAddress = await deployLnDefaultBridge(w, network.deployer, "ln-default-logic-v1.0.5"); + const logicAddress = await deployLnOppositeBridge(w, network.deployer, "ln-opposite-logic-v1.0.5"); + // upgrade + const proxyAdmin = await ethers.getContractAt("ProxyAdmin", "0xE3979fFa68BBa1F53c6F502c8F5788B370d28730", w); + //await proxyAdmin.upgrade("0x54cc9716905ba8ebdD01E6364125cA338Cd0054E", logicAddress); + await proxyAdmin.upgrade("0x79e6f452f1e491a7aF0382FA0a6EF9368691960D", logicAddress); + console.log("finished"); + } } main() diff --git a/helix-contract/deploy/deploy_ln_test.js b/helix-contract/deploy/deploy_ln_test.js new file mode 100644 index 00000000..96378fdb --- /dev/null +++ b/helix-contract/deploy/deploy_ln_test.js @@ -0,0 +1,451 @@ +const ethUtil = require('ethereumjs-util'); +const abi = require('ethereumjs-abi'); +const secp256k1 = require('secp256k1'); + +const privateKey = process.env.PRIKEY + +const initTransferId = "0x0000000000000000000000000000000000000000000000000000000000000000"; + +const networks = { + "goerli": { + url: "https://rpc.ankr.com/eth_goerli", + defaultBridge: "0x54cc9716905ba8ebdD01E6364125cA338Cd0054E", + oppositeBridge: "0x79e6f452f1e491a7aF0382FA0a6EF9368691960D", + chainId: 5, + usdc: "0x1a70127284B774fF4A4dbfe0115114642f0eca65", + usdt: "0x2303e4d55BF16a897Cb5Ab71c6225399509d9314", + }, + "arbitrum": { + url: "https://goerli-rollup.arbitrum.io/rpc", + defaultBridge: "0x54cc9716905ba8ebdD01E6364125cA338Cd0054E", + oppositeBridge: "0x79e6f452f1e491a7aF0382FA0a6EF9368691960D", + chainId: 421613, + usdc: "0x39dE82E1d9B8F62E11022FC3FC127a82F93fE47E", + usdt: "0x6d828718c1097A4C573bc25c638Cc05bF10dFeAF", + }, + "linea": { + url: "https://rpc.goerli.linea.build", + defaultBridge: "0x54cc9716905ba8ebdD01E6364125cA338Cd0054E", + oppositeBridge: "0x79e6f452f1e491a7aF0382FA0a6EF9368691960D", + chainId: 59140, + usdc: "0xb5E028f980dF5533cB0e8F04530B76637383d993", + usdt: "0xBC1A2f123Dc9CD2ec8d3cE42eF16c28F3C9bA686", + }, + "mantle": { + url: "https://rpc.testnet.mantle.xyz", + defaultBridge: "0x54cc9716905ba8ebdD01E6364125cA338Cd0054E", + oppositeBridge: "0x79e6f452f1e491a7aF0382FA0a6EF9368691960D", + chainId: 5001, + usdc: "0x0258Eb547bFEd540ed17843658C018569fe1E328", + usdt: "0x5F8D4232367759bCe5d9488D3ade77FCFF6B9b6B", + }, +}; + +function wait(ms) { + return new Promise(resolve => setTimeout(() => resolve(), ms)); +}; + +function getProviderKey( + remoteChainId, + provider, + sourceToken, + remoteToken +) { + const encode = ethers.utils.solidityPack([ + "uint256", + "address", + "address", + "address", + ], [remoteChainId, provider, sourceToken, remoteToken]); + return ethUtil.keccak256(encode); +} + +function getLockKey( + remoteChainId, + sourceToken, + remoteToken +) { + const encode = ethers.utils.solidityPack([ + "uint256", + "address", + "address", + ], [remoteChainId, sourceToken, remoteToken]); + return ethUtil.keccak256(encode); +} + +async function defaultTransferAndLockMargin( + wallet, + bridgeAddress, + remoteChainId, + sourceToken, + targetToken, + amount) { + const bridge = await ethers.getContractAt("LnDefaultBridge", bridgeAddress, wallet); + const expectedFee = await bridge.totalFee( + remoteChainId, + wallet.address, + sourceToken, + targetToken, + amount); + console.log("expect fee is", expectedFee); + const providerInfo = await bridge.srcProviders(getProviderKey(remoteChainId, wallet.address, sourceToken, targetToken)); + const expectedWithdrawNonce = providerInfo.config.withdrawNonce; + console.log("expect withdraw nonce is", expectedWithdrawNonce); + //const tx = await bridge.callStatic.transferAndLockMargin( + const tx = await bridge.transferAndLockMargin( + [ + remoteChainId, + wallet.address, + sourceToken, + targetToken, + providerInfo.lastTransferId, + expectedFee, + expectedWithdrawNonce, + ], + amount, + wallet.address, + ); + console.log(tx); +} + +async function oppositeTransferAndLockMargin( + wallet, + bridgeAddress, + remoteChainId, + sourceToken, + targetToken, + amount) { + const bridge = await ethers.getContractAt("LnOppositeBridge", bridgeAddress, wallet); + const expectedFee = await bridge.totalFee( + remoteChainId, + wallet.address, + sourceToken, + targetToken, + amount); + console.log("expect fee is", expectedFee); + const providerInfo = await bridge.srcProviders(getProviderKey(remoteChainId, wallet.address, sourceToken, targetToken)); + const expectedMargin = providerInfo.config.margin; + console.log("expect margin is", expectedMargin); + //const tx = await bridge.callStatic.transferAndLockMargin( + const tx = await bridge.transferAndLockMargin( + [ + remoteChainId, + wallet.address, + sourceToken, + targetToken, + providerInfo.lastTransferId, + expectedFee, + expectedMargin, + ], + amount, + wallet.address, + ); + console.log(tx); +} + +async function defaultRelay( + remoteChainId, + fromWallet, + toWallet, + fromBridgeAddress, + toBridgeAddress, + sourceToken, + targetToken, + amount, + previousTransferId, + expectedTransferId, +) { + const fromBridge = await ethers.getContractAt("LnDefaultBridge", fromBridgeAddress, fromWallet); + const lockInfo = await fromBridge.lockInfos(expectedTransferId); + const toBridge = await ethers.getContractAt("LnDefaultBridge", toBridgeAddress, toWallet); + //const tx = await bridge.callStatic.relay( + await toBridge.transferAndReleaseMargin( + [ + previousTransferId, + toWallet.address, + sourceToken, + targetToken, + amount, + lockInfo.timestamp, + toWallet.address, + ], + remoteChainId, + expectedTransferId, + ); + //console.log(tx); +} + +async function oppositeRelay( + remoteChainId, + fromWallet, + toWallet, + fromBridgeAddress, + toBridgeAddress, + sourceToken, + targetToken, + amount, + previousTransferId, + expectedTransferId, +) { + const fromBridge = await ethers.getContractAt("LnOppositeBridge", fromBridgeAddress, fromWallet); + const lockInfo = await fromBridge.lockInfos(expectedTransferId); + const toBridge = await ethers.getContractAt("LnOppositeBridge", toBridgeAddress, toWallet); + //const tx = await bridge.callStatic.relay( + await toBridge.transferAndReleaseMargin( + [ + previousTransferId, + toWallet.address, + sourceToken, + targetToken, + amount, + lockInfo.timestamp, + toWallet.address, + ], + remoteChainId, + expectedTransferId, + ); + //console.log(tx); +} + +async function slash( + wallet, + bridgeAddress, + provider, + sourceToken, + targetToken, + previousTransferId, + timestamp, + receiver, + amount, + expectedTransferId, +) { + const bridge = await ethers.getContractAt("Linea2EthTarget", bridgeAddress, wallet); + const cost = ethers.utils.parseEther("0.0003"); + //return; + + //const tx = await bridge.callStatic.slashAndRemoteRefund( + await bridge.slashAndRemoteRefund( + [ + previousTransferId, + provider, + sourceToken, + targetToken, + amount, + timestamp, + receiver, + ], + expectedTransferId, + {value: cost }, + ); + //console.log(tx); +} + +async function requestWithdrawMargin( + wallet, + bridgeAddress, + lastTransferId, + sourceToken, + amount, +) { + const bridge = await ethers.getContractAt("Linea2EthTarget", bridgeAddress, wallet); + const cost = 0; + //const tx = await bridge.callStatic.requestWithdrawMargin( + await bridge.requestWithdrawMargin( + lastTransferId, + sourceToken, + amount, + {value: cost }, + ); + //console.log(tx); +} + +function wallet(network) { + const provider = new ethers.providers.JsonRpcProvider(network.url); + const wallet = new ethers.Wallet(privateKey, provider); + return wallet; +} + +async function withdraw(bridgeType, from, to, sourceToken, targetToken, amount, fee, extParams) { + const sourceTokenContract = await ethers.getContractAt("Erc20", sourceToken, from.wallet); + if (bridgeType == "default") { + const srcDecimals = await sourceTokenContract.decimals(); + const formatedAmount = ethers.utils.parseUnits(amount, srcDecimals); + // withdraw from source bridge + const bridge = await ethers.getContractAt("LnDefaultBridge", from.defaultBridge, from.wallet); + await bridge.requestWithdrawMargin( + to.chainId, + sourceToken, + targetToken, + formatedAmount, + extParams, + { value: fee }, + ); + } else { + const dstDecimals = await sourceTokenContract.decimals(); + const formatedAmount = ethers.utils.parseUnits(amount, dstDecimals); + // withdraw from target bridge + const srcBridge = await ethers.getContractAt("LnOppositeBridge", from.oppositeBridge, from.wallet); + const providerInfo = await srcBridge.srcProviders(getProviderKey(to.chainId, from.wallet.address, sourceToken, targetToken)); + const bridge = await ethers.getContractAt("LnOppositeBridge", to.oppositeBridge, to.wallet); + await bridge.requestWithdrawMargin( + from.chainId, + providerInfo.lastTransferId, + sourceToken, + targetToken, + formatedAmount, + extParams, + { value: fee }, + ); + } +} + +async function transfer(bridgeType, from, to, sourceToken, targetToken, amount) { + const sourceTokenContract = await ethers.getContractAt("Erc20", sourceToken, from.wallet); + const decimals = await sourceTokenContract.decimals(); + const formatedAmount = ethers.utils.parseUnits(amount, decimals); + if (bridgeType == "default") { + const bridge = await ethers.getContractAt("LnDefaultBridge", from.defaultBridge, from.wallet); + const previousInfo = await bridge.srcProviders(getProviderKey(to.chainId, from.wallet.address, sourceToken, targetToken)); + await defaultTransferAndLockMargin( + from.wallet, + from.defaultBridge, + to.chainId, + sourceToken, + targetToken, + formatedAmount + ); + console.log("[default] transfer and lock margin successed"); + await wait(10000); + // query and relay + /* + const providerInfo = await bridge.srcProviders(getProviderKey(to.chainId, from.wallet.address, sourceToken, targetToken)); + const expectedTransferId = providerInfo.lastTransferId; + await defaultRelay( + from.chainId, + from.wallet, + to.wallet, + from.defaultBridge, + to.defaultBridge, + sourceToken, + targetToken, + formatedAmount, + previousInfo.lastTransferId, + expectedTransferId, + ); + console.log("[default] relay and release margin successed"); + */ + } else { + const bridge = await ethers.getContractAt("LnOppositeBridge", from.oppositeBridge, from.wallet); + const previousInfo = await bridge.srcProviders(getProviderKey(to.chainId, from.wallet.address, sourceToken, targetToken)); + await oppositeTransferAndLockMargin( + from.wallet, + from.oppositeBridge, + to.chainId, + sourceToken, + targetToken, + formatedAmount + ); + console.log("[opposite] transfer and lock margin successed"); + // query and relay + /* + const providerInfo = await bridge.srcProviders(getProviderKey(to.chainId, from.wallet.address, sourceToken, targetToken)); + const expectedTransferId = providerInfo.lastTransferId; + await oppositeRelay( + from.chainId, + from.wallet, + to.wallet, + to.oppositeBridge, + to.oppositeBridge, + sourceToken, + targetToken, + formatedAmount, + previousInfo.lastTransferId, + expectedTransferId, + ); + console.log("[opposite] relay and release margin successed"); + */ + } +} + +async function lzFee(from, to) { + const fillAddress = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + const bridge = await ethers.getContractAt("LnDefaultBridge", from.defaultBridge, from.wallet); + const serviceAddress = (await bridge.messagers(to.chainId)).sendService; + const messager = await ethers.getContractAt("LayerZeroMessager", serviceAddress, from.wallet); + const message = await bridge.encodeWithdrawCall("0x1000000000000000000000000000000000000000000000000000000000000001", 1, fillAddress, fillAddress, fillAddress, 1000000000); + const lzPayload = ethers.utils.defaultAbiCoder.encode([ + "address", + "address", + "bytes", + ], [fillAddress, fillAddress, message]); + return await messager.fee(to.chainId, lzPayload); +} + +async function eth2arbFee(arb, eth, sourceToken, targetToken) { + const l1GasPrice = 2000000000; + const l2GasPrice = 100000000; + const l2GasLimit = 1000000; + const bridge = await ethers.getContractAt("LnOppositeBridge", eth.oppositeBridge, eth.wallet); + const srcBridge = await ethers.getContractAt("LnOppositeBridge", arb.oppositeBridge, arb.wallet); + console.log(eth.chainId, arb.wallet.address, sourceToken, targetToken); + + const providerInfo = await srcBridge.srcProviders(getProviderKey(eth.chainId, arb.wallet.address, sourceToken, targetToken)) + const serviceAddress = (await bridge.messagers(arb.chainId)).sendService; + const messager = await ethers.getContractAt("Eth2ArbSendService", serviceAddress, eth.wallet); + const message = await bridge.encodeWithdrawMargin(providerInfo.lastTransferId, sourceToken, targetToken, 1000000000); + const fee = await messager.fee(message.length, l1GasPrice, l2GasPrice, l2GasLimit, 10); + const extParams = await messager.encodeParams( + fee[0], + l2GasPrice, + l2GasLimit, + eth.wallet.address + ); + return [fee[1], extParams]; +} + +// 2. deploy mapping token factory +async function main() { + for (let network in networks) { + networks[network]['wallet'] = wallet(networks[network]); + } + + //await transfer("default", networks.goerli, networks.linea, networks.goerli.usdt, networks.linea.usdt, "320"); + //await transfer("opposite", networks.linea, networks.goerli, networks.linea.usdt, networks.goerli.usdt, "500"); + //await transfer("default", networks.goerli, networks.mantle, networks.goerli.usdc, networks.mantle.usdc, 7000000); + //await transfer("default",networks.mantle, networks.goerli, networks.mantle.usdc, networks.goerli.usdc, "132"); + //console.log("transfer and relay successed"); + //return; + + /* + const fee = await lzFee(networks.arbitrum, networks.linea); + console.log(fee); + await withdraw("default", networks.arbitrum, networks.linea, networks.arbitrum.usdc, networks.linea.usdc, "20", fee.nativeFee, networks.arbitrum.wallet.address); + */ + const sendInfo = await eth2arbFee(networks.arbitrum, networks.goerli, networks.arbitrum.usdc, networks.goerli.usdc); + console.log(sendInfo); + await withdraw("opposite", networks.arbitrum, networks.goerli, networks.arbitrum.usdc, networks.goerli.usdc, "20", sendInfo[0], sendInfo[1]); + return; + + // slasher + await slash( + ethereumWallet, + ethereumLnBridgeAddress, + ethereumWallet.address, + usdcLineaAddress, + usdcEthereumAddress, + lastTransferId, + timestamp, + lineaWallet.address, + amount1, + expectedTransferId, + ); + console.log("slash successed"); + return; +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/helix-contract/deploy/flatten-ln.sh b/helix-contract/deploy/flatten-ln.sh index 3546082c..2ba89180 100644 --- a/helix-contract/deploy/flatten-ln.sh +++ b/helix-contract/deploy/flatten-ln.sh @@ -4,3 +4,4 @@ yarn flat contracts/ln/LnDefaultBridge.sol --output $path/LnDefaultBridge.sol yarn flat contracts/ln/LnOppositeBridge.sol --output $path/LnOppositeBridge.sol yarn flat contracts/ln/messager/Eth2LineaSendService.sol --output $path/Eth2LineaSendService.sol yarn flat contracts/ln/messager/Eth2LineaReceiveService.sol --output $path/Eth2LineaReceiveService.sol +yarn flat contracts/ln/messager/LayerZeroMessager.sol --output $path/LayerZeroMessager.sol diff --git a/helix-contract/flatten/lnv2/Eth2LineaReceiveService.sol b/helix-contract/flatten/lnv2/Eth2LineaReceiveService.sol index eeb0fe9f..1a9ab9ea 100644 --- a/helix-contract/flatten/lnv2/Eth2LineaReceiveService.sol +++ b/helix-contract/flatten/lnv2/Eth2LineaReceiveService.sol @@ -14,7 +14,7 @@ * '----------------' '----------------' '----------------' '----------------' '----------------' ' * * - * 9/11/2023 + * 9/12/2023 **/ pragma solidity ^0.8.10; diff --git a/helix-contract/flatten/lnv2/Eth2LineaSendService.sol b/helix-contract/flatten/lnv2/Eth2LineaSendService.sol index 1ea4869a..25460b3d 100644 --- a/helix-contract/flatten/lnv2/Eth2LineaSendService.sol +++ b/helix-contract/flatten/lnv2/Eth2LineaSendService.sol @@ -14,7 +14,7 @@ * '----------------' '----------------' '----------------' '----------------' '----------------' ' * * - * 9/11/2023 + * 9/12/2023 **/ pragma solidity ^0.8.10; diff --git a/helix-contract/flatten/lnv2/LayerZeroMessager.sol b/helix-contract/flatten/lnv2/LayerZeroMessager.sol new file mode 100644 index 00000000..3fd18147 --- /dev/null +++ b/helix-contract/flatten/lnv2/LayerZeroMessager.sol @@ -0,0 +1,334 @@ +// SPDX-License-Identifier: MIT + +/** + * .----------------. .----------------. .----------------. .----------------. .----------------. + * | .--------------. || .--------------. || .--------------. || .--------------. || .--------------. | + * | | ____ ____ | || | _________ | || | _____ | || | _____ | || | ____ ____ | | + * | | |_ || _| | || | |_ ___ | | || | |_ _| | || | |_ _| | || | |_ _||_ _| | | + * | | | |__| | | || | | |_ \_| | || | | | | || | | | | || | \ \ / / | | + * | | | __ | | || | | _| _ | || | | | _ | || | | | | || | > `' < | | + * | | _| | | |_ | || | _| |___/ | | || | _| |__/ | | || | _| |_ | || | _/ /'`\ \_ | | + * | | |____||____| | || | |_________| | || | |________| | || | |_____| | || | |____||____| | | + * | | | || | | || | | || | | || | | | + * | '--------------' || '--------------' || '--------------' || '--------------' || '--------------' | + * '----------------' '----------------' '----------------' '----------------' '----------------' ' + * + * + * 9/12/2023 + **/ + +pragma solidity ^0.8.10; + +// File contracts/ln/interface/ILowLevelMessager.sol +// License-Identifier: MIT + +interface ILowLevelMessageSender { + function registerRemoteReceiver(uint256 remoteChainId, address remoteBridge) external; + function sendMessage(uint256 remoteChainId, bytes memory message, bytes memory params) external payable; +} + +interface ILowLevelMessageReceiver { + function registerRemoteSender(uint256 remoteChainId, address remoteBridge) external; + function recvMessage(address remoteSender, address localReceiver, bytes memory payload) external; +} + +// File @zeppelin-solidity/contracts/utils/Context.sol@v4.7.3 +// License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (utils/Context.sol) + + +/** + * @dev Provides information about the current execution context, including the + * sender of the transaction and its data. While these are generally available + * via msg.sender and msg.data, they should not be accessed in such a direct + * manner, since when dealing with meta-transactions the account sending and + * paying for execution may not be the actual sender (as far as an application + * is concerned). + * + * This contract is only required for intermediate, library-like contracts. + */ +abstract contract Context { + function _msgSender() internal view virtual returns (address) { + return msg.sender; + } + + function _msgData() internal view virtual returns (bytes calldata) { + return msg.data; + } +} + +// File @zeppelin-solidity/contracts/security/Pausable.sol@v4.7.3 +// License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.7.0) (security/Pausable.sol) + + +/** + * @dev Contract module which allows children to implement an emergency stop + * mechanism that can be triggered by an authorized account. + * + * This module is used through inheritance. It will make available the + * modifiers `whenNotPaused` and `whenPaused`, which can be applied to + * the functions of your contract. Note that they will not be pausable by + * simply including this module, only once the modifiers are put in place. + */ +abstract contract Pausable is Context { + /** + * @dev Emitted when the pause is triggered by `account`. + */ + event Paused(address account); + + /** + * @dev Emitted when the pause is lifted by `account`. + */ + event Unpaused(address account); + + bool private _paused; + + /** + * @dev Initializes the contract in unpaused state. + */ + constructor() { + _paused = false; + } + + /** + * @dev Modifier to make a function callable only when the contract is not paused. + * + * Requirements: + * + * - The contract must not be paused. + */ + modifier whenNotPaused() { + _requireNotPaused(); + _; + } + + /** + * @dev Modifier to make a function callable only when the contract is paused. + * + * Requirements: + * + * - The contract must be paused. + */ + modifier whenPaused() { + _requirePaused(); + _; + } + + /** + * @dev Returns true if the contract is paused, and false otherwise. + */ + function paused() public view virtual returns (bool) { + return _paused; + } + + /** + * @dev Throws if the contract is paused. + */ + function _requireNotPaused() internal view virtual { + require(!paused(), "Pausable: paused"); + } + + /** + * @dev Throws if the contract is not paused. + */ + function _requirePaused() internal view virtual { + require(paused(), "Pausable: not paused"); + } + + /** + * @dev Triggers stopped state. + * + * Requirements: + * + * - The contract must not be paused. + */ + function _pause() internal virtual whenNotPaused { + _paused = true; + emit Paused(_msgSender()); + } + + /** + * @dev Returns to normal state. + * + * Requirements: + * + * - The contract must be paused. + */ + function _unpause() internal virtual whenPaused { + _paused = false; + emit Unpaused(_msgSender()); + } +} + +// File contracts/ln/base/LnAccessController.sol +// License-Identifier: MIT + +/// @title LnAccessController +/// @notice LnAccessController is a contract to control the access permission +/// @dev See https://github.com/helix-bridge/contracts/tree/master/helix-contract +contract LnAccessController is Pausable { + address public dao; + address public operator; + + modifier onlyDao() { + require(msg.sender == dao, "!dao"); + _; + } + + modifier onlyOperator() { + require(msg.sender == operator, "!operator"); + _; + } + + function _initialize(address _dao) internal { + dao = _dao; + operator = msg.sender; + } + + function setOperator(address _operator) onlyDao external { + operator = _operator; + } + + function transferOwnership(address _dao) onlyDao external { + dao = _dao; + } + + function unpause() external onlyOperator { + _unpause(); + } + + function pause() external onlyOperator { + _pause(); + } +} + +// File contracts/ln/messager/interface/ILayerZeroEndpoint.sol +// License-Identifier: MIT + +interface ILayerZeroEndpoint { + function send( + uint16 _dstChainId, + bytes calldata _destination, + bytes calldata _payload, + address payable _refundAddress, + address _zroPaymentAddress, + bytes calldata _adapterParams + ) external payable; + + function estimateFees( + uint16 _dstChainId, + address _userApplication, + bytes calldata _payload, + bool _payInZRO, + bytes calldata _adapterParam + ) external view returns (uint nativeFee, uint zroFee); +} + +// File contracts/ln/messager/LayerZeroMessager.sol +// License-Identifier: MIT + + + +contract LayerZeroMessager is LnAccessController { + ILayerZeroEndpoint public endpoint; + + struct RemoteMessager { + uint16 lzRemoteChainId; + address messager; + } + + // app remoteChainId => layerzero remote messager + mapping(uint256=>RemoteMessager) public remoteMessagers; + // lz remoteChainId => trustedRemotes + mapping(uint16=>bytes32) public trustedRemotes; + + // token bridge pair + // hash(lzRemoteChainId, localAppAddress) => remoteAppAddress + mapping(bytes32=>address) public remoteAppReceivers; + mapping(bytes32=>address) public remoteAppSenders; + + event CallResult(uint16 lzRemoteChainId, bytes srcAddress, bool successed); + + constructor(address _dao, address _endpoint) { + _initialize(_dao); + endpoint = ILayerZeroEndpoint(_endpoint); + } + + modifier onlyRemoteBridge(uint16 lzRemoteChainId, bytes calldata srcAddress) { + require(msg.sender == address(endpoint), "invalid caller"); + require(trustedRemotes[lzRemoteChainId] == keccak256(srcAddress), "invalid remote caller"); + _; + } + + function setRemoteMessager(uint256 _appRemoteChainId, uint16 _lzRemoteChainId, address _remoteMessager) onlyOperator external { + remoteMessagers[_appRemoteChainId] = RemoteMessager(_lzRemoteChainId, _remoteMessager); + trustedRemotes[_lzRemoteChainId] = keccak256(abi.encodePacked(_remoteMessager, address(this))); + } + + function registerRemoteReceiver(uint256 _remoteChainId, address _remoteBridge) external { + RemoteMessager memory remoteMessager = remoteMessagers[_remoteChainId]; + require(remoteMessager.messager != address(0), "remote not configured"); + bytes32 key = keccak256(abi.encodePacked(remoteMessager.lzRemoteChainId, msg.sender)); + remoteAppReceivers[key] = _remoteBridge; + } + + function registerRemoteSender(uint256 _remoteChainId, address _remoteBridge) external { + RemoteMessager memory remoteMessager = remoteMessagers[_remoteChainId]; + require(remoteMessager.messager != address(0), "remote not configured"); + bytes32 key = keccak256(abi.encodePacked(remoteMessager.lzRemoteChainId, msg.sender)); + remoteAppSenders[key] = _remoteBridge; + } + + function sendMessage(uint256 _remoteChainId, bytes memory _message, bytes memory _params) external payable { + address refunder = address(bytes20(_params)); + RemoteMessager memory remoteMessager = remoteMessagers[_remoteChainId]; + require(remoteMessager.messager != address(0), "remote not configured"); + bytes memory destination = abi.encodePacked( + remoteMessager.messager, + address(this) + ); + bytes32 key = keccak256(abi.encodePacked(remoteMessager.lzRemoteChainId, msg.sender)); + address remoteAppAddress = remoteAppReceivers[key]; + require(remoteAppAddress != address(0), "app pair not registered"); + bytes memory lzPayload = abi.encode(msg.sender, remoteAppAddress, _message); + endpoint.send{ value: msg.value }( + remoteMessager.lzRemoteChainId, + destination, + lzPayload, + payable(refunder), + // zro payment, future parameter + address(0x0), + bytes("") + ); + } + + function lzReceive( + uint16 _srcChainId, + bytes calldata _srcAddress, + uint64, //nonce unused + bytes calldata _payload) onlyRemoteBridge(_srcChainId, _srcAddress) external { + // call + (address remoteAppAddress, address localAppAddress, bytes memory message) = abi.decode(_payload, (address, address, bytes)); + bytes32 key = keccak256(abi.encodePacked(_srcChainId, localAppAddress)); + require(remoteAppAddress == remoteAppSenders[key], "invalid remote address"); + (bool success,) = localAppAddress.call(message); + // don't revert to prevent message block + emit CallResult(_srcChainId, _srcAddress, success); + } + + function fee( + uint256 _remoteChainId, + bytes memory _message + ) external view returns(uint256 nativeFee, uint256 zroFee) { + RemoteMessager memory remoteMessager = remoteMessagers[_remoteChainId]; + require(remoteMessager.messager != address(0), "messager not configured"); + return endpoint.estimateFees( + remoteMessager.lzRemoteChainId, + remoteMessager.messager, + _message, + false, + bytes("") + ); + } +} \ No newline at end of file diff --git a/helix-contract/flatten/lnv2/LnDefaultBridge.sol b/helix-contract/flatten/lnv2/LnDefaultBridge.sol index 5f08b155..68cd1478 100644 --- a/helix-contract/flatten/lnv2/LnDefaultBridge.sol +++ b/helix-contract/flatten/lnv2/LnDefaultBridge.sol @@ -14,7 +14,7 @@ * '----------------' '----------------' '----------------' '----------------' '----------------' ' * * - * 9/11/2023 + * 9/12/2023 **/ pragma solidity ^0.8.10; @@ -607,17 +607,175 @@ contract LnDefaultBridgeSource { } } -// File contracts/ln/interface/ILowLevelMessager.sol +// File @zeppelin-solidity/contracts/utils/Context.sol@v4.7.3 // License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (utils/Context.sol) -interface ILowLevelMessageSender { - function registerRemoteReceiver(uint256 remoteChainId, address remoteBridge) external; - function sendMessage(uint256 remoteChainId, bytes memory message, bytes memory params) external payable; + +/** + * @dev Provides information about the current execution context, including the + * sender of the transaction and its data. While these are generally available + * via msg.sender and msg.data, they should not be accessed in such a direct + * manner, since when dealing with meta-transactions the account sending and + * paying for execution may not be the actual sender (as far as an application + * is concerned). + * + * This contract is only required for intermediate, library-like contracts. + */ +abstract contract Context { + function _msgSender() internal view virtual returns (address) { + return msg.sender; + } + + function _msgData() internal view virtual returns (bytes calldata) { + return msg.data; + } } -interface ILowLevelMessageReceiver { - function registerRemoteSender(uint256 remoteChainId, address remoteBridge) external; - function recvMessage(address remoteSender, address localReceiver, bytes memory payload) external; +// File @zeppelin-solidity/contracts/security/Pausable.sol@v4.7.3 +// License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.7.0) (security/Pausable.sol) + + +/** + * @dev Contract module which allows children to implement an emergency stop + * mechanism that can be triggered by an authorized account. + * + * This module is used through inheritance. It will make available the + * modifiers `whenNotPaused` and `whenPaused`, which can be applied to + * the functions of your contract. Note that they will not be pausable by + * simply including this module, only once the modifiers are put in place. + */ +abstract contract Pausable is Context { + /** + * @dev Emitted when the pause is triggered by `account`. + */ + event Paused(address account); + + /** + * @dev Emitted when the pause is lifted by `account`. + */ + event Unpaused(address account); + + bool private _paused; + + /** + * @dev Initializes the contract in unpaused state. + */ + constructor() { + _paused = false; + } + + /** + * @dev Modifier to make a function callable only when the contract is not paused. + * + * Requirements: + * + * - The contract must not be paused. + */ + modifier whenNotPaused() { + _requireNotPaused(); + _; + } + + /** + * @dev Modifier to make a function callable only when the contract is paused. + * + * Requirements: + * + * - The contract must be paused. + */ + modifier whenPaused() { + _requirePaused(); + _; + } + + /** + * @dev Returns true if the contract is paused, and false otherwise. + */ + function paused() public view virtual returns (bool) { + return _paused; + } + + /** + * @dev Throws if the contract is paused. + */ + function _requireNotPaused() internal view virtual { + require(!paused(), "Pausable: paused"); + } + + /** + * @dev Throws if the contract is not paused. + */ + function _requirePaused() internal view virtual { + require(paused(), "Pausable: not paused"); + } + + /** + * @dev Triggers stopped state. + * + * Requirements: + * + * - The contract must not be paused. + */ + function _pause() internal virtual whenNotPaused { + _paused = true; + emit Paused(_msgSender()); + } + + /** + * @dev Returns to normal state. + * + * Requirements: + * + * - The contract must be paused. + */ + function _unpause() internal virtual whenPaused { + _paused = false; + emit Unpaused(_msgSender()); + } +} + +// File contracts/ln/base/LnAccessController.sol +// License-Identifier: MIT + +/// @title LnAccessController +/// @notice LnAccessController is a contract to control the access permission +/// @dev See https://github.com/helix-bridge/contracts/tree/master/helix-contract +contract LnAccessController is Pausable { + address public dao; + address public operator; + + modifier onlyDao() { + require(msg.sender == dao, "!dao"); + _; + } + + modifier onlyOperator() { + require(msg.sender == operator, "!operator"); + _; + } + + function _initialize(address _dao) internal { + dao = _dao; + operator = msg.sender; + } + + function setOperator(address _operator) onlyDao external { + operator = _operator; + } + + function transferOwnership(address _dao) onlyDao external { + dao = _dao; + } + + function unpause() external onlyOperator { + _unpause(); + } + + function pause() external onlyOperator { + _pause(); + } } // File contracts/ln/base/LnDefaultBridgeTarget.sol @@ -847,175 +1005,17 @@ contract LnDefaultBridgeTarget { } } -// File @zeppelin-solidity/contracts/utils/Context.sol@v4.7.3 -// License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (utils/Context.sol) - - -/** - * @dev Provides information about the current execution context, including the - * sender of the transaction and its data. While these are generally available - * via msg.sender and msg.data, they should not be accessed in such a direct - * manner, since when dealing with meta-transactions the account sending and - * paying for execution may not be the actual sender (as far as an application - * is concerned). - * - * This contract is only required for intermediate, library-like contracts. - */ -abstract contract Context { - function _msgSender() internal view virtual returns (address) { - return msg.sender; - } - - function _msgData() internal view virtual returns (bytes calldata) { - return msg.data; - } -} - -// File @zeppelin-solidity/contracts/security/Pausable.sol@v4.7.3 +// File contracts/ln/interface/ILowLevelMessager.sol // License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.7.0) (security/Pausable.sol) - - -/** - * @dev Contract module which allows children to implement an emergency stop - * mechanism that can be triggered by an authorized account. - * - * This module is used through inheritance. It will make available the - * modifiers `whenNotPaused` and `whenPaused`, which can be applied to - * the functions of your contract. Note that they will not be pausable by - * simply including this module, only once the modifiers are put in place. - */ -abstract contract Pausable is Context { - /** - * @dev Emitted when the pause is triggered by `account`. - */ - event Paused(address account); - - /** - * @dev Emitted when the pause is lifted by `account`. - */ - event Unpaused(address account); - - bool private _paused; - - /** - * @dev Initializes the contract in unpaused state. - */ - constructor() { - _paused = false; - } - - /** - * @dev Modifier to make a function callable only when the contract is not paused. - * - * Requirements: - * - * - The contract must not be paused. - */ - modifier whenNotPaused() { - _requireNotPaused(); - _; - } - - /** - * @dev Modifier to make a function callable only when the contract is paused. - * - * Requirements: - * - * - The contract must be paused. - */ - modifier whenPaused() { - _requirePaused(); - _; - } - - /** - * @dev Returns true if the contract is paused, and false otherwise. - */ - function paused() public view virtual returns (bool) { - return _paused; - } - - /** - * @dev Throws if the contract is paused. - */ - function _requireNotPaused() internal view virtual { - require(!paused(), "Pausable: paused"); - } - - /** - * @dev Throws if the contract is not paused. - */ - function _requirePaused() internal view virtual { - require(paused(), "Pausable: not paused"); - } - /** - * @dev Triggers stopped state. - * - * Requirements: - * - * - The contract must not be paused. - */ - function _pause() internal virtual whenNotPaused { - _paused = true; - emit Paused(_msgSender()); - } - - /** - * @dev Returns to normal state. - * - * Requirements: - * - * - The contract must be paused. - */ - function _unpause() internal virtual whenPaused { - _paused = false; - emit Unpaused(_msgSender()); - } +interface ILowLevelMessageSender { + function registerRemoteReceiver(uint256 remoteChainId, address remoteBridge) external; + function sendMessage(uint256 remoteChainId, bytes memory message, bytes memory params) external payable; } -// File contracts/ln/base/LnAccessController.sol -// License-Identifier: MIT - -/// @title LnAccessController -/// @notice LnAccessController is a contract to control the access permission -/// @dev See https://github.com/helix-bridge/contracts/tree/master/helix-contract -contract LnAccessController is Pausable { - address public dao; - address public operator; - - modifier onlyDao() { - require(msg.sender == dao, "!dao"); - _; - } - - modifier onlyOperator() { - require(msg.sender == operator, "!operator"); - _; - } - - function _initialize(address _dao) internal { - dao = _dao; - operator = msg.sender; - } - - function setOperator(address _operator) onlyDao external { - operator = _operator; - } - - function transferOwnership(address _dao) onlyDao external { - dao = _dao; - } - - function unpause() external onlyOperator { - _unpause(); - } - - function pause() external onlyOperator { - _pause(); - } +interface ILowLevelMessageReceiver { + function registerRemoteSender(uint256 remoteChainId, address remoteBridge) external; + function recvMessage(address remoteSender, address localReceiver, bytes memory payload) external; } // File @zeppelin-solidity/contracts/utils/Address.sol@v4.7.3 @@ -1439,7 +1439,7 @@ contract LnDefaultBridge is Initializable, LnAccessController, LnDefaultBridgeSo function _sendMessageToTarget(uint256 _remoteChainId, bytes memory _payload, bytes memory _extParams) internal override { address sendService = messagers[_remoteChainId].sendService; require(sendService != address(0), "invalid messager"); - ILowLevelMessageSender(sendService).sendMessage(_remoteChainId, _payload, _extParams); + ILowLevelMessageSender(sendService).sendMessage{value: msg.value}(_remoteChainId, _payload, _extParams); } function _verifyRemote(uint256 _remoteChainId) whenNotPaused internal view override { diff --git a/helix-contract/flatten/lnv2/LnOppositeBridge.sol b/helix-contract/flatten/lnv2/LnOppositeBridge.sol index 4ea95370..3e109853 100644 --- a/helix-contract/flatten/lnv2/LnOppositeBridge.sol +++ b/helix-contract/flatten/lnv2/LnOppositeBridge.sol @@ -14,7 +14,7 @@ * '----------------' '----------------' '----------------' '----------------' '----------------' ' * * - * 9/11/2023 + * 9/12/2023 **/ pragma solidity ^0.8.10; @@ -190,6 +190,19 @@ contract LnAccessController is Pausable { } } +// File contracts/ln/interface/ILowLevelMessager.sol +// License-Identifier: MIT + +interface ILowLevelMessageSender { + function registerRemoteReceiver(uint256 remoteChainId, address remoteBridge) external; + function sendMessage(uint256 remoteChainId, bytes memory message, bytes memory params) external payable; +} + +interface ILowLevelMessageReceiver { + function registerRemoteSender(uint256 remoteChainId, address remoteBridge) external; + function recvMessage(address remoteSender, address localReceiver, bytes memory payload) external; +} + // File @zeppelin-solidity/contracts/token/ERC20/IERC20.sol@v4.7.3 // License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.6.0) (token/ERC20/IERC20.sol) @@ -377,616 +390,606 @@ library LnBridgeHelper { } } -// File contracts/ln/base/LnOppositeBridgeSource.sol +// File contracts/ln/interface/ILnOppositeBridgeSource.sol // License-Identifier: MIT -/// @title LnBridgeSource -/// @notice LnBridgeSource is a contract to help user transfer token to liquidity node and generate proof, -/// then the liquidity node must transfer the same amount of the token to the user on target chain. -/// Otherwise if timeout the slasher can paid for relayer and slash the transfer, then request slash from lnProvider's margin. -/// @dev See https://github.com/helix-bridge/contracts/tree/master/helix-contract -contract LnOppositeBridgeSource { - // the Liquidity Node provider info - // Liquidity Node need register first - struct SourceProviderConfigure { - uint112 margin; - uint112 baseFee; - // liquidityFeeRate / 100,000 * amount = liquidityFee - // the max liquidity fee rate is 0.255% - uint16 liquidityFeeRate; - bool pause; - } - struct SourceProviderInfo { - SourceProviderConfigure config; - bytes32 lastTransferId; - } - - // the Snapshot is the state of the token bridge when user prepare to transfer across chains. - // If the snapshot updated when the across chain transfer confirmed, it will - // 1. if lastTransferId updated, revert - // 2. if margin decrease or totalFee increase, revert - // 3. if margin increase or totalFee decrease, success - struct Snapshot { - uint256 remoteChainId; - address provider; - address sourceToken; - address targetToken; - bytes32 transferId; - uint112 totalFee; - uint112 depositedMargin; - } - // registered token info - // tokenKey => token info - mapping(bytes32=>LnBridgeHelper.TokenInfo) public tokenInfos; - // registered srcProviders - mapping(bytes32=>SourceProviderInfo) public srcProviders; - // each time cross chain transfer, amount and fee can't be larger than type(uint112).max - struct LockInfo { - // amount + providerFee + penaltyLnCollateral - // the Indexer should be care about this value, it will frozen lnProvider's margin when the transfer not finished. - // and when the slasher slash success, this amount of token will be transfer from lnProvider's margin to slasher. - uint112 amountWithFeeAndPenalty; - uint32 timestamp; - bool hasSlashed; - } - // key: transferId = hash(proviousTransferId, targetToken, receiver, targetAmount) - // * `proviousTransferId` is used to ensure the continuous of the transfer - // * `timestamp` is the block.timestmap to judge timeout on target chain(here we support source and target chain has the same world clock) - // * `targetToken`, `receiver` and `targetAmount` are used on target chain to transfer target token. - mapping(bytes32 => LockInfo) public lockInfos; - address public protocolFeeReceiver; +interface ILnOppositeBridgeSource { + function slash( + bytes32 lastRefundTransferId, + bytes32 transferId, + uint256 remoteChainId, + uint256 timestamp, + address sourceToken, + address targetToken, + address provider, + address slasher + ) external; - event TokenLocked( + function withdrawMargin( + bytes32 lastRefundTransferId, + bytes32 lastTransferId, uint256 remoteChainId, - bytes32 transferId, address provider, address sourceToken, address targetToken, - uint112 amount, - uint112 fee, - uint64 timestamp, - address receiver); - event LiquidityWithdrawn(uint256 remoteChainId, address provider, address sourceToken, address targetToken, uint112 amount); - event Slash(uint256 remoteChainId, bytes32 transferId, address provider, address sourceToken, address targetToken, uint112 margin, address slasher); - // relayer - event LnProviderUpdated(uint256 remoteChainId, address provider, address sourceToken, address targetToken, uint112 margin, uint112 baseFee, uint16 liquidityfeeRate); + uint112 amount + ) external; +} - modifier allowRemoteCall(uint256 _remoteChainId) { - _verifyRemote(_remoteChainId); - _; - } +// File contracts/ln/base/LnOppositeBridgeTarget.sol +// License-Identifier: MIT - function _verifyRemote(uint256 _remoteChainId) internal virtual {} - function _updateFeeReceiver(address _feeReceiver) internal { - require(_feeReceiver != address(this), "invalid system fee receiver"); - protocolFeeReceiver = _feeReceiver; +contract LnOppositeBridgeTarget { + // if slasher == address(0), this FillTransfer is relayed by lnProvider + // otherwise, this FillTransfer is slashed by slasher + // if there is no slash transfer before, then it's latestSlashTransferId is assigned by INIT_SLASH_TRANSFER_ID, a special flag + struct SlashInfo { + address provider; + address sourceToken; + address targetToken; + address slasher; + uint256 timestamp; } - function _setTokenInfo( - uint256 _remoteChainId, - address _sourceToken, - address _targetToken, - uint112 _protocolFee, - uint112 _penaltyLnCollateral, - uint8 _sourceDecimals, - uint8 _targetDecimals + // transferId => latest slash transfer Id + mapping(bytes32 => bytes32) public fillTransfers; + // transferId => Slash info + mapping(bytes32 => SlashInfo) public slashInfos; + + event TransferFilled(bytes32 transferId, address slasher); + event SlashRequest(uint256 remoteChainId, address sourceToken, address targetToken, bytes32 transferId); + event WithdrawMarginRequest(uint256 remoteChainId, address sourceToken, address targetToken, uint112 amount); + + // if slasher is nonzero, then it's a slash fill transfer + function _checkPreviousAndFillTransfer( + bytes32 _transferId, + bytes32 _previousTransferId ) internal { - bytes32 tokenKey = LnBridgeHelper.getTokenKey(_remoteChainId, _sourceToken, _targetToken); - tokenInfos[tokenKey] = LnBridgeHelper.TokenInfo( - _protocolFee, - _penaltyLnCollateral, - _sourceDecimals, - _targetDecimals, - true - ); - } + // the first fill transfer, we fill the INIT_SLASH_TRANSFER_ID as the latest slash transferId + if (_previousTransferId == bytes32(0)) { + fillTransfers[_transferId] = LnBridgeHelper.INIT_SLASH_TRANSFER_ID; + } else { + // Find the previous slash fill, it is a slash fill if the slasher is not zero address. + bytes32 previousLatestSlashTransferId = fillTransfers[_previousTransferId]; + require(previousLatestSlashTransferId != bytes32(0), "previous fill not exist"); - function providerPause(uint256 _remoteChainId, address _sourceToken, address _targetToken) external { - bytes32 providerKey = LnBridgeHelper.getProviderKey(_remoteChainId, msg.sender, _sourceToken, _targetToken); - srcProviders[providerKey].config.pause = true; - } + SlashInfo memory previousSlashInfo = slashInfos[_previousTransferId]; + // we use latestSlashTransferId to store the latest slash transferId + // if previous.slasher != 0, then previous is slashed + // if previous.slasher == 0, then previous is not slashed + bytes32 latestSlashTransferId = previousSlashInfo.slasher != address(0) ? _previousTransferId : previousLatestSlashTransferId; - function providerUnpause(uint256 _remoteChainId, address _sourceToken, address _targetToken) external { - bytes32 providerKey = LnBridgeHelper.getProviderKey(_remoteChainId, msg.sender, _sourceToken, _targetToken); - srcProviders[providerKey].config.pause = false; + fillTransfers[_transferId] = latestSlashTransferId; + } } - // lnProvider can register or update its configure by using this function - // * `margin` is the increased value of the deposited margin - function updateProviderFeeAndMargin( + // fill transfer + // 1. if transfer is not slashed or relayed, LnProvider relay message to fill the transfer, and the transfer finished on target chain + // 2. if transfer is timeout and not processed, slasher(any account) can fill the transfer and request slash + // if it's filled by slasher, we store the address of the slasher + // expectedTransferId used to ensure the parameter is the same as on source chain + // some cases + // 1) If transferId is not exist on source chain, it'll be rejected by source chain when shashed. + // 2) If transferId exist on source chain. We have the same hash process on source and target chain, so the previousTransferId is trusted. + // 2.1) If transferId is the first transfer Id of this provider, then previousTransferId is zero and the latestSlashTransferId is INIT_SLASH_TRANSFER_ID + // 2.2) If transferId is not the first transfer, then it's latestSlashTransferId has the next two scenarios + // * the previousTransfer is a slash transfer, then latestSlashTransferId is previousTransferId + // * the previousTransfer is a normal relayed transfer, then latestSlashTransferId is previousTransfer's latestSlashTransferId + // I. transferId is trusted => previousTransferId is trusted => previousTransfer.previousTransferId is trusted => ... => firstTransfer is trusted + // II. transferId is trusted => previousTransferId is trusted => latestSlashTransferId is trusted if previousTransfer is a slash transfer + // III. Both I and II => latestSlashTransferId is trusted if previousTransfer is normal relayed tranfer + function _fillTransfer( + LnBridgeHelper.TransferParameter calldata _params, uint256 _remoteChainId, - address _sourceToken, - address _targetToken, - uint112 _margin, - uint112 _baseFee, - uint16 _liquidityFeeRate - ) external payable { - require(_liquidityFeeRate < LnBridgeHelper.LIQUIDITY_FEE_RATE_BASE, "liquidity fee too large"); - bytes32 tokenKey = LnBridgeHelper.getTokenKey(_remoteChainId, _sourceToken, _targetToken); - LnBridgeHelper.TokenInfo memory tokenInfo = tokenInfos[tokenKey]; - require(tokenInfo.isRegistered, "token is not registered"); - - bytes32 providerKey = LnBridgeHelper.getProviderKey(_remoteChainId, msg.sender, _sourceToken, _targetToken); - SourceProviderInfo memory providerInfo = srcProviders[providerKey]; - - SourceProviderConfigure memory config = SourceProviderConfigure( - // the margin can be only increased here - _margin + providerInfo.config.margin, - _baseFee, - _liquidityFeeRate, - providerInfo.config.pause - ); + bytes32 _expectedTransferId + ) internal { + bytes32 transferId = keccak256(abi.encodePacked( + _remoteChainId, + block.chainid, + _params.previousTransferId, + _params.provider, + _params.sourceToken, + _params.targetToken, + _params.receiver, + _params.amount)); + require(_expectedTransferId == transferId, "check expected transferId failed"); + // Make sure this transfer was never filled before + require(fillTransfers[transferId] == bytes32(0), "fill exist"); - srcProviders[providerKey].config = config; + _checkPreviousAndFillTransfer(transferId, _params.previousTransferId); - if (_sourceToken == address(0)) { - require(msg.value == _margin, "invalid margin value"); + if (_params.targetToken == address(0)) { + require(msg.value >= _params.amount, "invalid amount"); + LnBridgeHelper.safeTransferNative(_params.receiver, _params.amount); } else { - if (_margin > 0) { - LnBridgeHelper.safeTransferFrom(_sourceToken, msg.sender, address(this), _margin); - } + LnBridgeHelper.safeTransferFrom(_params.targetToken, msg.sender, _params.receiver, uint256(_params.amount)); } - emit LnProviderUpdated(_remoteChainId, msg.sender, _sourceToken, _targetToken, config.margin, _baseFee, _liquidityFeeRate); } - // the fee user should paid when transfer. - // totalFee = providerFee + protocolFee - // providerFee = provider.baseFee + provider.liquidityFeeRate * amount - function totalFee( + function transferAndReleaseMargin( + LnBridgeHelper.TransferParameter calldata _params, uint256 _remoteChainId, - address _provider, - address _sourceToken, - address _targetToken, - uint112 _amount - ) external view returns(uint256) { - bytes32 providerKey = LnBridgeHelper.getProviderKey(_remoteChainId, _provider, _sourceToken, _targetToken); - SourceProviderInfo memory providerInfo = srcProviders[providerKey]; - uint112 providerFee = LnBridgeHelper.calculateProviderFee(providerInfo.config.baseFee, providerInfo.config.liquidityFeeRate, _amount); - bytes32 tokenKey = LnBridgeHelper.getTokenKey(_remoteChainId, _sourceToken, _targetToken); - return providerFee + tokenInfos[tokenKey].protocolFee; - } - - // This function transfers tokens from the user to LnProvider and generates a proof on the source chain. - // The snapshot represents the state of the LN bridge for this LnProvider, obtained by the off-chain indexer. - // If the chain state is updated and does not match the snapshot state, the transaction will be reverted. - // 1. the state(lastTransferId, fee, margin) must match snapshot - // 2. transferId not exist - function transferAndLockMargin( - Snapshot calldata _snapshot, - uint112 _amount, - address _receiver - ) external payable { - require(_amount > 0, "invalid amount"); - - bytes32 providerKey = LnBridgeHelper.getProviderKey(_snapshot.remoteChainId, _snapshot.provider, _snapshot.sourceToken, _snapshot.targetToken); - SourceProviderInfo memory providerInfo = srcProviders[providerKey]; + bytes32 _expectedTransferId + ) payable external { + // normal relay message, fill slasher as zero + require(_params.provider == msg.sender, "invalid provider"); + _fillTransfer(_params, _remoteChainId, _expectedTransferId); - require(!providerInfo.config.pause, "provider paused"); + emit TransferFilled(_expectedTransferId, address(0)); + } - LnBridgeHelper.TokenInfo memory tokenInfo = tokenInfos[ - LnBridgeHelper.getTokenKey(_snapshot.remoteChainId, _snapshot.sourceToken, _snapshot.targetToken) - ]; + // The condition for slash is that the transfer has timed out + // Meanwhile we need to request a slash transaction to the source chain to withdraw the LnProvider's margin + // On the source chain, we need to verify all the transfers before has been relayed or slashed. + // So we needs to carry the the previous shash transferId to ensure that the slash is continuous. + function _slashAndRemoteReleaseCall( + LnBridgeHelper.TransferParameter calldata _params, + uint256 _remoteChainId, + bytes32 _expectedTransferId + ) internal returns(bytes memory message) { + require(block.timestamp > _params.timestamp + LnBridgeHelper.SLASH_EXPIRE_TIME, "slash time not expired"); + _fillTransfer(_params, _remoteChainId, _expectedTransferId); - uint112 providerFee = LnBridgeHelper.calculateProviderFee(providerInfo.config.baseFee, providerInfo.config.liquidityFeeRate, _amount); - - // the chain state not match snapshot - require(providerInfo.lastTransferId == _snapshot.transferId, "snapshot expired"); - // Note: this requirement is not enough to ensure that the lnProvider's margin is enough because there maybe some frozen margins in other transfers - require(providerInfo.config.margin >= _amount + tokenInfo.penaltyLnCollateral + providerFee, "amount not valid"); - require(_snapshot.depositedMargin <= providerInfo.config.margin, "margin updated"); - require(_snapshot.totalFee >= tokenInfo.protocolFee + providerFee, "fee is invalid"); - - uint112 targetAmount = LnBridgeHelper.sourceAmountToTargetAmount(tokenInfo, _amount); - require(targetAmount > 0, "invalid amount"); - require(block.timestamp < type(uint32).max, "timestamp overflow"); - bytes32 transferId = keccak256(abi.encodePacked( - block.chainid, - _snapshot.remoteChainId, - _snapshot.transferId, - _snapshot.provider, - _snapshot.sourceToken, - _snapshot.targetToken, - _receiver, - targetAmount)); - require(lockInfos[transferId].timestamp == 0, "transferId exist"); - lockInfos[transferId] = LockInfo(_amount + tokenInfo.penaltyLnCollateral + providerFee, uint32(block.timestamp), false); + // slasher = msg.sender + slashInfos[_expectedTransferId] = SlashInfo(_params.provider, _params.sourceToken, _params.targetToken, msg.sender, _params.timestamp); - // update the state to prevent other transfers using the same snapshot - srcProviders[providerKey].lastTransferId = transferId; + // Do not slash `transferId` in source chain unless `latestSlashTransferId` has been slashed + message = encodeSlashCall( + fillTransfers[_expectedTransferId], + _expectedTransferId, + _params.timestamp, + _params.sourceToken, + _params.targetToken, + _params.provider, + msg.sender + ); + emit TransferFilled(_expectedTransferId, msg.sender); + } - if (_snapshot.sourceToken == address(0)) { - require(_amount + _snapshot.totalFee == msg.value, "amount unmatched"); - LnBridgeHelper.safeTransferNative(_snapshot.provider, _amount + providerFee); - if (tokenInfo.protocolFee > 0) { - LnBridgeHelper.safeTransferNative(protocolFeeReceiver, tokenInfo.protocolFee); - } - uint256 refund = _snapshot.totalFee - tokenInfo.protocolFee - providerFee; - if ( refund > 0 ) { - LnBridgeHelper.safeTransferNative(msg.sender, refund); - } - } else { - LnBridgeHelper.safeTransferFrom( - _snapshot.sourceToken, - msg.sender, - _snapshot.provider, - _amount + providerFee - ); - if (tokenInfo.protocolFee > 0) { - LnBridgeHelper.safeTransferFrom( - _snapshot.sourceToken, - msg.sender, - protocolFeeReceiver, - tokenInfo.protocolFee - ); - } - } - emit TokenLocked( - _snapshot.remoteChainId, - transferId, - _snapshot.provider, - _snapshot.sourceToken, - _snapshot.targetToken, - targetAmount, - providerFee, - uint64(block.timestamp), - _receiver); + // we use this to verify that the transfer has been slashed by user and it can resend the slash request + function _retrySlashAndRemoteReleaseCall(bytes32 _transferId) internal view returns(bytes memory message) { + bytes32 latestSlashTransferId = fillTransfers[_transferId]; + // transfer must be filled + require(latestSlashTransferId != bytes32(0), "invalid transfer id"); + // transfer must be slashed + SlashInfo memory slashInfo = slashInfos[_transferId]; + require(slashInfo.slasher != address(0), "slasher not exist"); + message = encodeSlashCall( + latestSlashTransferId, + _transferId, + slashInfo.timestamp, + slashInfo.sourceToken, + slashInfo.targetToken, + slashInfo.provider, + slashInfo.slasher + ); } - // this slash is called by remote message - // the token should be sent to the slasher who slash and finish the transfer on target chain. - // latestSlashTransferId is the latest slashed transfer trusted from the target chain, and the current slash transfer cannot be executed before the latestSlash transfer. - // after slash, the margin of lnProvider need to be updated - function slash( + function encodeSlashCall( bytes32 _latestSlashTransferId, bytes32 _transferId, - uint256 _remoteChainId, uint256 _timestamp, address _sourceToken, address _targetToken, address _provider, address _slasher - ) external allowRemoteCall(_remoteChainId) { - // check lastTransfer - // ensure last slash transfer(checked on target chain) has been slashed - LockInfo memory lastLockInfo = lockInfos[_latestSlashTransferId]; - require(lastLockInfo.hasSlashed || _latestSlashTransferId == LnBridgeHelper.INIT_SLASH_TRANSFER_ID, "latest slash transfer invalid"); - LockInfo memory lockInfo = lockInfos[_transferId]; - - // ensure transfer exist and not slashed yet - require(!lockInfo.hasSlashed, "transfer has been slashed"); - require(lockInfo.timestamp > 0 && lockInfo.timestamp == _timestamp, "lnBridgeSource:invalid timestamp"); - - bytes32 providerKey = LnBridgeHelper.getProviderKey(_remoteChainId, _provider, _sourceToken, _targetToken); + ) public view returns(bytes memory) { + return abi.encodeWithSelector( + ILnOppositeBridgeSource.slash.selector, + _latestSlashTransferId, + _transferId, + block.chainid, + _timestamp, + _sourceToken, + _targetToken, + _provider, + _slasher + ); + } - SourceProviderInfo memory lnProvider = srcProviders[providerKey]; - lockInfos[_transferId].hasSlashed = true; - // transfer token to the slasher - uint112 slashAmount = lockInfo.amountWithFeeAndPenalty; - require(lnProvider.config.margin >= slashAmount, "margin not enough"); - uint112 updatedMargin = lnProvider.config.margin - slashAmount; - srcProviders[providerKey].config.margin = updatedMargin; + function _sendMessageToTarget(uint256 _remoteChainId, bytes memory _payload, bytes memory _extParams) internal virtual {} - if (_sourceToken == address(0)) { - LnBridgeHelper.safeTransferNative(_slasher, slashAmount); - } else { - LnBridgeHelper.safeTransfer(_sourceToken, _slasher, slashAmount); - } + function requestSlashAndRemoteRelease( + LnBridgeHelper.TransferParameter calldata _params, + uint256 _remoteChainId, + bytes32 _expectedTransferId, + bytes memory _extParams + ) payable external { + bytes memory slashCallMessage = _slashAndRemoteReleaseCall( + _params, + _remoteChainId, + _expectedTransferId + ); + _sendMessageToTarget(_remoteChainId, slashCallMessage, _extParams); + emit SlashRequest(_remoteChainId, _params.sourceToken, _params.targetToken, _expectedTransferId); + } - emit Slash(_remoteChainId, _transferId, _provider, _sourceToken, _targetToken, updatedMargin, _slasher); + function requestRetrySlashAndRemoteRelease( + uint256 remoteChainId, + bytes32 _transferId, + bytes memory _extParams + ) payable external { + bytes memory retryCallMessage = _retrySlashAndRemoteReleaseCall(_transferId); + _sendMessageToTarget(remoteChainId, retryCallMessage, _extParams); } - // lastTransfer is the latest slash transfer, all transfer must be relayed or slashed - // if user use the snapshot before this transaction to send cross-chain transfer, it should be reverted because this `withdrawMargin` will decrease margin. - function withdrawMargin( - bytes32 _latestSlashTransferId, + function encodeWithdrawMargin( bytes32 _lastTransferId, - uint256 _remoteChainId, - address _provider, address _sourceToken, address _targetToken, uint112 _amount - ) external allowRemoteCall(_remoteChainId) { - // check the latest slash transfer - // ensure latest slash tranfer(verified on target chain) has been slashed on source chain - LockInfo memory lastRefundLockInfo = lockInfos[_latestSlashTransferId]; - require(lastRefundLockInfo.hasSlashed || _latestSlashTransferId == LnBridgeHelper.INIT_SLASH_TRANSFER_ID, "latest slash transfer invalid"); - - // use this condition to ensure that the withdraw message is sent by the provider - // the parameter provider is the message sender of this remote withdraw call - bytes32 providerKey = LnBridgeHelper.getProviderKey(_remoteChainId, _provider, _sourceToken, _targetToken); - SourceProviderInfo memory lnProvider = srcProviders[providerKey]; - - // ensure all transfer has finished - require(lnProvider.lastTransferId == _lastTransferId, "invalid last transferid"); - require(lnProvider.config.margin >= _amount, "margin not enough"); - uint112 updatedMargin = lnProvider.config.margin - _amount; - srcProviders[providerKey].config.margin = updatedMargin; - if (_sourceToken == address(0)) { - LnBridgeHelper.safeTransferNative(_provider, _amount); - } else { - LnBridgeHelper.safeTransfer(_sourceToken, _provider, _amount); + ) public view returns(bytes memory message) { + bytes32 latestSlashTransferId = LnBridgeHelper.INIT_SLASH_TRANSFER_ID; + if (_lastTransferId != bytes32(0)) { + latestSlashTransferId = fillTransfers[_lastTransferId]; + require(latestSlashTransferId != bytes32(0), "invalid last transfer"); } - emit LiquidityWithdrawn(_remoteChainId, _provider, _sourceToken, _targetToken, updatedMargin); - } -} - -// File contracts/ln/interface/ILowLevelMessager.sol -// License-Identifier: MIT -interface ILowLevelMessageSender { - function registerRemoteReceiver(uint256 remoteChainId, address remoteBridge) external; - function sendMessage(uint256 remoteChainId, bytes memory message, bytes memory params) external payable; -} + return abi.encodeWithSelector( + ILnOppositeBridgeSource.withdrawMargin.selector, + latestSlashTransferId, + _lastTransferId, + block.chainid, + msg.sender, + _sourceToken, + _targetToken, + _amount + ); + } -interface ILowLevelMessageReceiver { - function registerRemoteSender(uint256 remoteChainId, address remoteBridge) external; - function recvMessage(address remoteSender, address localReceiver, bytes memory payload) external; + function requestWithdrawMargin( + uint256 _remoteChainId, + bytes32 _lastTransferId, + address _sourceToken, + address _targetToken, + uint112 _amount, + bytes memory _extParams + ) payable external { + bytes memory withdrawCallMessage = encodeWithdrawMargin( + _lastTransferId, + _sourceToken, + _targetToken, + _amount + ); + _sendMessageToTarget(_remoteChainId, withdrawCallMessage, _extParams); + emit WithdrawMarginRequest(_remoteChainId, _sourceToken, _targetToken, _amount); + } } -// File contracts/ln/interface/ILnOppositeBridgeSource.sol +// File contracts/ln/base/LnOppositeBridgeSource.sol // License-Identifier: MIT +/// @title LnBridgeSource +/// @notice LnBridgeSource is a contract to help user transfer token to liquidity node and generate proof, +/// then the liquidity node must transfer the same amount of the token to the user on target chain. +/// Otherwise if timeout the slasher can paid for relayer and slash the transfer, then request slash from lnProvider's margin. +/// @dev See https://github.com/helix-bridge/contracts/tree/master/helix-contract +contract LnOppositeBridgeSource { + // the Liquidity Node provider info + // Liquidity Node need register first + struct SourceProviderConfigure { + uint112 margin; + uint112 baseFee; + // liquidityFeeRate / 100,000 * amount = liquidityFee + // the max liquidity fee rate is 0.255% + uint16 liquidityFeeRate; + bool pause; + } + struct SourceProviderInfo { + SourceProviderConfigure config; + bytes32 lastTransferId; + } + + // the Snapshot is the state of the token bridge when user prepare to transfer across chains. + // If the snapshot updated when the across chain transfer confirmed, it will + // 1. if lastTransferId updated, revert + // 2. if margin decrease or totalFee increase, revert + // 3. if margin increase or totalFee decrease, success + struct Snapshot { + uint256 remoteChainId; + address provider; + address sourceToken; + address targetToken; + bytes32 transferId; + uint112 totalFee; + uint112 depositedMargin; + } + // registered token info + // tokenKey => token info + mapping(bytes32=>LnBridgeHelper.TokenInfo) public tokenInfos; + // registered srcProviders + mapping(bytes32=>SourceProviderInfo) public srcProviders; + // each time cross chain transfer, amount and fee can't be larger than type(uint112).max + struct LockInfo { + // amount + providerFee + penaltyLnCollateral + // the Indexer should be care about this value, it will frozen lnProvider's margin when the transfer not finished. + // and when the slasher slash success, this amount of token will be transfer from lnProvider's margin to slasher. + uint112 amountWithFeeAndPenalty; + uint32 timestamp; + bool hasSlashed; + } + // key: transferId = hash(proviousTransferId, targetToken, receiver, targetAmount) + // * `proviousTransferId` is used to ensure the continuous of the transfer + // * `timestamp` is the block.timestmap to judge timeout on target chain(here we support source and target chain has the same world clock) + // * `targetToken`, `receiver` and `targetAmount` are used on target chain to transfer target token. + mapping(bytes32 => LockInfo) public lockInfos; -interface ILnOppositeBridgeSource { - function slash( - bytes32 lastRefundTransferId, - bytes32 transferId, - uint256 remoteChainId, - uint256 timestamp, - address sourceToken, - address targetToken, - address provider, - address slasher - ) external; + address public protocolFeeReceiver; - function withdrawMargin( - bytes32 lastRefundTransferId, - bytes32 lastTransferId, + event TokenLocked( uint256 remoteChainId, + bytes32 transferId, address provider, address sourceToken, address targetToken, - uint112 amount - ) external; -} - -// File contracts/ln/base/LnOppositeBridgeTarget.sol -// License-Identifier: MIT - + uint112 amount, + uint112 fee, + uint64 timestamp, + address receiver); + event LiquidityWithdrawn(uint256 remoteChainId, address provider, address sourceToken, address targetToken, uint112 amount); + event Slash(uint256 remoteChainId, bytes32 transferId, address provider, address sourceToken, address targetToken, uint112 margin, address slasher); + // relayer + event LnProviderUpdated(uint256 remoteChainId, address provider, address sourceToken, address targetToken, uint112 margin, uint112 baseFee, uint16 liquidityfeeRate); -contract LnOppositeBridgeTarget { - // if slasher == address(0), this FillTransfer is relayed by lnProvider - // otherwise, this FillTransfer is slashed by slasher - // if there is no slash transfer before, then it's latestSlashTransferId is assigned by INIT_SLASH_TRANSFER_ID, a special flag - struct SlashInfo { - address provider; - address sourceToken; - address targetToken; - address slasher; - uint256 timestamp; + modifier allowRemoteCall(uint256 _remoteChainId) { + _verifyRemote(_remoteChainId); + _; } - // transferId => latest slash transfer Id - mapping(bytes32 => bytes32) public fillTransfers; - // transferId => Slash info - mapping(bytes32 => SlashInfo) public slashInfos; + function _verifyRemote(uint256 _remoteChainId) internal virtual {} - event TransferFilled(bytes32 transferId, address slasher); - event SlashRequest(uint256 remoteChainId, address sourceToken, address targetToken, bytes32 transferId); - event WithdrawMarginRequest(uint256 remoteChainId, address sourceToken, address targetToken, uint112 amount); + function _updateFeeReceiver(address _feeReceiver) internal { + require(_feeReceiver != address(this), "invalid system fee receiver"); + protocolFeeReceiver = _feeReceiver; + } - // if slasher is nonzero, then it's a slash fill transfer - function _checkPreviousAndFillTransfer( - bytes32 _transferId, - bytes32 _previousTransferId + function _setTokenInfo( + uint256 _remoteChainId, + address _sourceToken, + address _targetToken, + uint112 _protocolFee, + uint112 _penaltyLnCollateral, + uint8 _sourceDecimals, + uint8 _targetDecimals ) internal { - // the first fill transfer, we fill the INIT_SLASH_TRANSFER_ID as the latest slash transferId - if (_previousTransferId == bytes32(0)) { - fillTransfers[_transferId] = LnBridgeHelper.INIT_SLASH_TRANSFER_ID; - } else { - // Find the previous slash fill, it is a slash fill if the slasher is not zero address. - bytes32 previousLatestSlashTransferId = fillTransfers[_previousTransferId]; - require(previousLatestSlashTransferId != bytes32(0), "previous fill not exist"); + bytes32 tokenKey = LnBridgeHelper.getTokenKey(_remoteChainId, _sourceToken, _targetToken); + tokenInfos[tokenKey] = LnBridgeHelper.TokenInfo( + _protocolFee, + _penaltyLnCollateral, + _sourceDecimals, + _targetDecimals, + true + ); + } - SlashInfo memory previousSlashInfo = slashInfos[_previousTransferId]; - // we use latestSlashTransferId to store the latest slash transferId - // if previous.slasher != 0, then previous is slashed - // if previous.slasher == 0, then previous is not slashed - bytes32 latestSlashTransferId = previousSlashInfo.slasher != address(0) ? _previousTransferId : previousLatestSlashTransferId; + function providerPause(uint256 _remoteChainId, address _sourceToken, address _targetToken) external { + bytes32 providerKey = LnBridgeHelper.getProviderKey(_remoteChainId, msg.sender, _sourceToken, _targetToken); + srcProviders[providerKey].config.pause = true; + } - fillTransfers[_transferId] = latestSlashTransferId; - } + function providerUnpause(uint256 _remoteChainId, address _sourceToken, address _targetToken) external { + bytes32 providerKey = LnBridgeHelper.getProviderKey(_remoteChainId, msg.sender, _sourceToken, _targetToken); + srcProviders[providerKey].config.pause = false; } - // fill transfer - // 1. if transfer is not slashed or relayed, LnProvider relay message to fill the transfer, and the transfer finished on target chain - // 2. if transfer is timeout and not processed, slasher(any account) can fill the transfer and request slash - // if it's filled by slasher, we store the address of the slasher - // expectedTransferId used to ensure the parameter is the same as on source chain - // some cases - // 1) If transferId is not exist on source chain, it'll be rejected by source chain when shashed. - // 2) If transferId exist on source chain. We have the same hash process on source and target chain, so the previousTransferId is trusted. - // 2.1) If transferId is the first transfer Id of this provider, then previousTransferId is zero and the latestSlashTransferId is INIT_SLASH_TRANSFER_ID - // 2.2) If transferId is not the first transfer, then it's latestSlashTransferId has the next two scenarios - // * the previousTransfer is a slash transfer, then latestSlashTransferId is previousTransferId - // * the previousTransfer is a normal relayed transfer, then latestSlashTransferId is previousTransfer's latestSlashTransferId - // I. transferId is trusted => previousTransferId is trusted => previousTransfer.previousTransferId is trusted => ... => firstTransfer is trusted - // II. transferId is trusted => previousTransferId is trusted => latestSlashTransferId is trusted if previousTransfer is a slash transfer - // III. Both I and II => latestSlashTransferId is trusted if previousTransfer is normal relayed tranfer - function _fillTransfer( - LnBridgeHelper.TransferParameter calldata _params, + // lnProvider can register or update its configure by using this function + // * `margin` is the increased value of the deposited margin + function updateProviderFeeAndMargin( uint256 _remoteChainId, - bytes32 _expectedTransferId - ) internal { - bytes32 transferId = keccak256(abi.encodePacked( - _remoteChainId, - block.chainid, - _params.previousTransferId, - _params.provider, - _params.sourceToken, - _params.targetToken, - _params.receiver, - _params.amount)); - require(_expectedTransferId == transferId, "check expected transferId failed"); - // Make sure this transfer was never filled before - require(fillTransfers[transferId] == bytes32(0), "fill exist"); + address _sourceToken, + address _targetToken, + uint112 _margin, + uint112 _baseFee, + uint16 _liquidityFeeRate + ) external payable { + require(_liquidityFeeRate < LnBridgeHelper.LIQUIDITY_FEE_RATE_BASE, "liquidity fee too large"); + bytes32 tokenKey = LnBridgeHelper.getTokenKey(_remoteChainId, _sourceToken, _targetToken); + LnBridgeHelper.TokenInfo memory tokenInfo = tokenInfos[tokenKey]; + require(tokenInfo.isRegistered, "token is not registered"); - _checkPreviousAndFillTransfer(transferId, _params.previousTransferId); + bytes32 providerKey = LnBridgeHelper.getProviderKey(_remoteChainId, msg.sender, _sourceToken, _targetToken); + SourceProviderInfo memory providerInfo = srcProviders[providerKey]; - if (_params.targetToken == address(0)) { - require(msg.value >= _params.amount, "invalid amount"); - LnBridgeHelper.safeTransferNative(_params.receiver, _params.amount); + SourceProviderConfigure memory config = SourceProviderConfigure( + // the margin can be only increased here + _margin + providerInfo.config.margin, + _baseFee, + _liquidityFeeRate, + providerInfo.config.pause + ); + + srcProviders[providerKey].config = config; + + if (_sourceToken == address(0)) { + require(msg.value == _margin, "invalid margin value"); } else { - LnBridgeHelper.safeTransferFrom(_params.targetToken, msg.sender, _params.receiver, uint256(_params.amount)); + if (_margin > 0) { + LnBridgeHelper.safeTransferFrom(_sourceToken, msg.sender, address(this), _margin); + } } + emit LnProviderUpdated(_remoteChainId, msg.sender, _sourceToken, _targetToken, config.margin, _baseFee, _liquidityFeeRate); } - function transferAndReleaseMargin( - LnBridgeHelper.TransferParameter calldata _params, + // the fee user should paid when transfer. + // totalFee = providerFee + protocolFee + // providerFee = provider.baseFee + provider.liquidityFeeRate * amount + function totalFee( uint256 _remoteChainId, - bytes32 _expectedTransferId - ) payable external { - // normal relay message, fill slasher as zero - require(_params.provider == msg.sender, "invalid provider"); - _fillTransfer(_params, _remoteChainId, _expectedTransferId); - - emit TransferFilled(_expectedTransferId, address(0)); + address _provider, + address _sourceToken, + address _targetToken, + uint112 _amount + ) external view returns(uint256) { + bytes32 providerKey = LnBridgeHelper.getProviderKey(_remoteChainId, _provider, _sourceToken, _targetToken); + SourceProviderInfo memory providerInfo = srcProviders[providerKey]; + uint112 providerFee = LnBridgeHelper.calculateProviderFee(providerInfo.config.baseFee, providerInfo.config.liquidityFeeRate, _amount); + bytes32 tokenKey = LnBridgeHelper.getTokenKey(_remoteChainId, _sourceToken, _targetToken); + return providerFee + tokenInfos[tokenKey].protocolFee; } - // The condition for slash is that the transfer has timed out - // Meanwhile we need to request a slash transaction to the source chain to withdraw the LnProvider's margin - // On the source chain, we need to verify all the transfers before has been relayed or slashed. - // So we needs to carry the the previous shash transferId to ensure that the slash is continuous. - function _slashAndRemoteReleaseCall( - LnBridgeHelper.TransferParameter calldata _params, - uint256 _remoteChainId, - bytes32 _expectedTransferId - ) internal returns(bytes memory message) { - require(block.timestamp > _params.timestamp + LnBridgeHelper.SLASH_EXPIRE_TIME, "slash time not expired"); - _fillTransfer(_params, _remoteChainId, _expectedTransferId); + // This function transfers tokens from the user to LnProvider and generates a proof on the source chain. + // The snapshot represents the state of the LN bridge for this LnProvider, obtained by the off-chain indexer. + // If the chain state is updated and does not match the snapshot state, the transaction will be reverted. + // 1. the state(lastTransferId, fee, margin) must match snapshot + // 2. transferId not exist + function transferAndLockMargin( + Snapshot calldata _snapshot, + uint112 _amount, + address _receiver + ) external payable { + require(_amount > 0, "invalid amount"); - // slasher = msg.sender - slashInfos[_expectedTransferId] = SlashInfo(_params.provider, _params.sourceToken, _params.targetToken, msg.sender, _params.timestamp); + bytes32 providerKey = LnBridgeHelper.getProviderKey(_snapshot.remoteChainId, _snapshot.provider, _snapshot.sourceToken, _snapshot.targetToken); + SourceProviderInfo memory providerInfo = srcProviders[providerKey]; - // Do not slash `transferId` in source chain unless `latestSlashTransferId` has been slashed - message = _encodeSlashCall( - fillTransfers[_expectedTransferId], - _expectedTransferId, - _params.timestamp, - _params.sourceToken, - _params.targetToken, - _params.provider, - msg.sender - ); - emit TransferFilled(_expectedTransferId, msg.sender); - } + require(!providerInfo.config.pause, "provider paused"); - // we use this to verify that the transfer has been slashed by user and it can resend the slash request - function _retrySlashAndRemoteReleaseCall(bytes32 _transferId) internal view returns(bytes memory message) { - bytes32 latestSlashTransferId = fillTransfers[_transferId]; - // transfer must be filled - require(latestSlashTransferId != bytes32(0), "invalid transfer id"); - // transfer must be slashed - SlashInfo memory slashInfo = slashInfos[_transferId]; - require(slashInfo.slasher != address(0), "slasher not exist"); - message = _encodeSlashCall( - latestSlashTransferId, - _transferId, - slashInfo.timestamp, - slashInfo.sourceToken, - slashInfo.targetToken, - slashInfo.provider, - slashInfo.slasher - ); + LnBridgeHelper.TokenInfo memory tokenInfo = tokenInfos[ + LnBridgeHelper.getTokenKey(_snapshot.remoteChainId, _snapshot.sourceToken, _snapshot.targetToken) + ]; + + uint112 providerFee = LnBridgeHelper.calculateProviderFee(providerInfo.config.baseFee, providerInfo.config.liquidityFeeRate, _amount); + + // the chain state not match snapshot + require(providerInfo.lastTransferId == _snapshot.transferId, "snapshot expired"); + // Note: this requirement is not enough to ensure that the lnProvider's margin is enough because there maybe some frozen margins in other transfers + require(providerInfo.config.margin >= _amount + tokenInfo.penaltyLnCollateral + providerFee, "amount not valid"); + require(_snapshot.depositedMargin <= providerInfo.config.margin, "margin updated"); + require(_snapshot.totalFee >= tokenInfo.protocolFee + providerFee, "fee is invalid"); + + uint112 targetAmount = LnBridgeHelper.sourceAmountToTargetAmount(tokenInfo, _amount); + require(targetAmount > 0, "invalid amount"); + require(block.timestamp < type(uint32).max, "timestamp overflow"); + bytes32 transferId = keccak256(abi.encodePacked( + block.chainid, + _snapshot.remoteChainId, + _snapshot.transferId, + _snapshot.provider, + _snapshot.sourceToken, + _snapshot.targetToken, + _receiver, + targetAmount)); + require(lockInfos[transferId].timestamp == 0, "transferId exist"); + lockInfos[transferId] = LockInfo(_amount + tokenInfo.penaltyLnCollateral + providerFee, uint32(block.timestamp), false); + + // update the state to prevent other transfers using the same snapshot + srcProviders[providerKey].lastTransferId = transferId; + + if (_snapshot.sourceToken == address(0)) { + require(_amount + _snapshot.totalFee == msg.value, "amount unmatched"); + LnBridgeHelper.safeTransferNative(_snapshot.provider, _amount + providerFee); + if (tokenInfo.protocolFee > 0) { + LnBridgeHelper.safeTransferNative(protocolFeeReceiver, tokenInfo.protocolFee); + } + uint256 refund = _snapshot.totalFee - tokenInfo.protocolFee - providerFee; + if ( refund > 0 ) { + LnBridgeHelper.safeTransferNative(msg.sender, refund); + } + } else { + LnBridgeHelper.safeTransferFrom( + _snapshot.sourceToken, + msg.sender, + _snapshot.provider, + _amount + providerFee + ); + if (tokenInfo.protocolFee > 0) { + LnBridgeHelper.safeTransferFrom( + _snapshot.sourceToken, + msg.sender, + protocolFeeReceiver, + tokenInfo.protocolFee + ); + } + } + emit TokenLocked( + _snapshot.remoteChainId, + transferId, + _snapshot.provider, + _snapshot.sourceToken, + _snapshot.targetToken, + targetAmount, + providerFee, + uint64(block.timestamp), + _receiver); } - function _encodeSlashCall( + // this slash is called by remote message + // the token should be sent to the slasher who slash and finish the transfer on target chain. + // latestSlashTransferId is the latest slashed transfer trusted from the target chain, and the current slash transfer cannot be executed before the latestSlash transfer. + // after slash, the margin of lnProvider need to be updated + function slash( bytes32 _latestSlashTransferId, bytes32 _transferId, + uint256 _remoteChainId, uint256 _timestamp, address _sourceToken, address _targetToken, address _provider, address _slasher - ) internal view returns(bytes memory) { - return abi.encodeWithSelector( - ILnOppositeBridgeSource.slash.selector, - _latestSlashTransferId, - _transferId, - block.chainid, - _timestamp, - _sourceToken, - _targetToken, - _provider, - _slasher - ); - } + ) external allowRemoteCall(_remoteChainId) { + // check lastTransfer + // ensure last slash transfer(checked on target chain) has been slashed + LockInfo memory lastLockInfo = lockInfos[_latestSlashTransferId]; + require(lastLockInfo.hasSlashed || _latestSlashTransferId == LnBridgeHelper.INIT_SLASH_TRANSFER_ID, "latest slash transfer invalid"); + LockInfo memory lockInfo = lockInfos[_transferId]; - function _sendMessageToTarget(uint256 _remoteChainId, bytes memory _payload, bytes memory _extParams) internal virtual {} + // ensure transfer exist and not slashed yet + require(!lockInfo.hasSlashed, "transfer has been slashed"); + require(lockInfo.timestamp > 0 && lockInfo.timestamp == _timestamp, "lnBridgeSource:invalid timestamp"); - function requestSlashAndRemoteRelease( - LnBridgeHelper.TransferParameter calldata _params, - uint256 _remoteChainId, - bytes32 _expectedTransferId, - bytes memory _extParams - ) payable external { - bytes memory slashCallMessage = _slashAndRemoteReleaseCall( - _params, - _remoteChainId, - _expectedTransferId - ); - _sendMessageToTarget(_remoteChainId, slashCallMessage, _extParams); - emit SlashRequest(_remoteChainId, _params.sourceToken, _params.targetToken, _expectedTransferId); - } + bytes32 providerKey = LnBridgeHelper.getProviderKey(_remoteChainId, _provider, _sourceToken, _targetToken); - function requestRetrySlashAndRemoteRelease( - uint256 remoteChainId, - bytes32 _transferId, - bytes memory _extParams - ) payable external { - bytes memory retryCallMessage = _retrySlashAndRemoteReleaseCall(_transferId); - _sendMessageToTarget(remoteChainId, retryCallMessage, _extParams); + SourceProviderInfo memory lnProvider = srcProviders[providerKey]; + lockInfos[_transferId].hasSlashed = true; + // transfer token to the slasher + uint112 slashAmount = lockInfo.amountWithFeeAndPenalty; + require(lnProvider.config.margin >= slashAmount, "margin not enough"); + uint112 updatedMargin = lnProvider.config.margin - slashAmount; + srcProviders[providerKey].config.margin = updatedMargin; + + if (_sourceToken == address(0)) { + LnBridgeHelper.safeTransferNative(_slasher, slashAmount); + } else { + LnBridgeHelper.safeTransfer(_sourceToken, _slasher, slashAmount); + } + + emit Slash(_remoteChainId, _transferId, _provider, _sourceToken, _targetToken, updatedMargin, _slasher); } - function _requestWithdrawMargin( + // lastTransfer is the latest slash transfer, all transfer must be relayed or slashed + // if user use the snapshot before this transaction to send cross-chain transfer, it should be reverted because this `withdrawMargin` will decrease margin. + function withdrawMargin( + bytes32 _latestSlashTransferId, bytes32 _lastTransferId, + uint256 _remoteChainId, + address _provider, address _sourceToken, address _targetToken, uint112 _amount - ) internal view returns(bytes memory message) { - bytes32 latestSlashTransferId = fillTransfers[_lastTransferId]; - require(latestSlashTransferId != bytes32(0), "invalid last transfer"); + ) external allowRemoteCall(_remoteChainId) { + // check the latest slash transfer + // ensure latest slash tranfer(verified on target chain) has been slashed on source chain + LockInfo memory lastRefundLockInfo = lockInfos[_latestSlashTransferId]; + require(lastRefundLockInfo.hasSlashed || _latestSlashTransferId == LnBridgeHelper.INIT_SLASH_TRANSFER_ID, "latest slash transfer invalid"); - return abi.encodeWithSelector( - ILnOppositeBridgeSource.withdrawMargin.selector, - latestSlashTransferId, - _lastTransferId, - block.chainid, - msg.sender, - _sourceToken, - _targetToken, - _amount - ); - } + // use this condition to ensure that the withdraw message is sent by the provider + // the parameter provider is the message sender of this remote withdraw call + bytes32 providerKey = LnBridgeHelper.getProviderKey(_remoteChainId, _provider, _sourceToken, _targetToken); + SourceProviderInfo memory lnProvider = srcProviders[providerKey]; - function requestWithdrawMargin( - uint256 _remoteChainId, - bytes32 _lastTransferId, - address _sourceToken, - address _targetToken, - uint112 _amount, - bytes memory _extParams - ) payable external { - bytes memory withdrawCallMessage = _requestWithdrawMargin( - _lastTransferId, - _sourceToken, - _targetToken, - _amount - ); - _sendMessageToTarget(_remoteChainId, withdrawCallMessage, _extParams); - emit WithdrawMarginRequest(_remoteChainId, _sourceToken, _targetToken, _amount); + // ensure all transfer has finished + require(lnProvider.lastTransferId == _lastTransferId, "invalid last transferid"); + require(lnProvider.config.margin >= _amount, "margin not enough"); + uint112 updatedMargin = lnProvider.config.margin - _amount; + srcProviders[providerKey].config.margin = updatedMargin; + if (_sourceToken == address(0)) { + LnBridgeHelper.safeTransferNative(_provider, _amount); + } else { + LnBridgeHelper.safeTransfer(_sourceToken, _provider, _amount); + } + emit LiquidityWithdrawn(_remoteChainId, _provider, _sourceToken, _targetToken, updatedMargin); } } @@ -1362,7 +1365,7 @@ contract LnOppositeBridge is Initializable, LnAccessController, LnOppositeBridge address sendService; address receiveService; } - mapping(uint256=>MessagerService) messagers; + mapping(uint256=>MessagerService) public messagers; receive() external payable {} @@ -1408,7 +1411,7 @@ contract LnOppositeBridge is Initializable, LnAccessController, LnOppositeBridge function _sendMessageToTarget(uint256 _remoteChainId, bytes memory _payload, bytes memory _extParams) internal override { address sendService = messagers[_remoteChainId].sendService; require(sendService != address(0), "invalid messager"); - ILowLevelMessageSender(sendService).sendMessage(_remoteChainId, _payload, _extParams); + ILowLevelMessageSender(sendService).sendMessage{value: msg.value}(_remoteChainId, _payload, _extParams); } function _verifyRemote(uint256 _remoteChainId) whenNotPaused internal view override {