diff --git a/helix-contract/address/ln-dev.json b/helix-contract/address/ln-dev.json index 58810345..4e31cb54 100644 --- a/helix-contract/address/ln-dev.json +++ b/helix-contract/address/ln-dev.json @@ -1,4 +1,24 @@ { + "chains": { + "sepolia": { + "name": "sepolia", + "url": "https://rpc2.sepolia.org", + "dao": "0x88a39B052d477CfdE47600a7C9950a441Ce61cb4", + "chainId": 11155111, + "lzChainId": 10161, + "lzEndpoint": "0xae92d5aD7583AD66E49A0c67BAd18F6ba52dDDc1", + "deployer": "0x80D4c766C5142D1313D531Afe7384D0D5E108Db3" + }, + "arbitrum-sepolia": { + "name": "arbitrum-sepolia", + "url": "https://sepolia-rollup.arbitrum.io/rpc", + "dao": "0x88a39B052d477CfdE47600a7C9950a441Ce61cb4", + "chainId": 421614, + "lzChainId": 10231, + "lzEndpoint": "0x6098e96a28E02f27B1e6BD381f870F1C8Bd169d3", + "deployer": "0x80D4c766C5142D1313D531Afe7384D0D5E108Db3" + } + }, "messagers": { "zksync-goerli": { "layerzeroMessager": "0x7e303b0A3F08F9fa5F5629Abb998B8Deba89049B" @@ -11,7 +31,9 @@ "debugMessager": "0x2e8D237226041FAFe3F66b6cfc54b064923D454E" }, "sepolia": { - "Eth2ScrollSendService": "0x89AF830781A2C1d3580Db930bea11094F55AfEae" + "chainId": 11155111, + "Eth2ScrollSendService": "0x89AF830781A2C1d3580Db930bea11094F55AfEae", + "layerzeroMessager": "0x33C9916a43507aa0a89a3e889522f840aa1245fE" }, "arbitrum-goerli": { "Eth2ArbReceiveService": "0x102F8D7Cfe692AA79c17E3958aB00D060Df0B88f", @@ -34,7 +56,9 @@ "layerzeroMessager": "0x463D1730a8527CA58d48EF70C7460B9920346567" }, "arbitrum-sepolia": { - "darwiniaMsglineMessager": "0xCddD3e43dA1e9485d4FcD3782DFba04aADCfC9B2" + "chainId": "421614", + "darwiniaMsglineMessager": "0xCddD3e43dA1e9485d4FcD3782DFba04aADCfC9B2", + "layerzeroMessager": "0x87A649246974732f7AbBe01F2DD81E3D829EF0B7" } }, "ProxyAdmin": { @@ -53,11 +77,11 @@ "LnOppositeBridgeProxy": "0x4C538EfA6e3f9Dfb939AA4F0B224577DA665923a", "LnV3BridgeLogic": { "zkSync": "0xa480d54af27d65cc485f7c11ec2F072F2d671863", - "others": "0x3B3Ad62319CAc1C1872C0370d84C60F816d4B69B" + "others": "0xC4Cecb7d4c0eA6c7AA88CbdE56612Cdc2DE2E756" }, "LnV3BridgeProxy": { "zkSync": "0xab38D0030cC28e413C4DD2B7D0ac2b6984e6d3f0", - "others": "0xb0Ce2498C2526cceA1D7792e4B62C3066Eb5529B" + "others": "0x38627Cb033De66a1E07e73f5D0a7a7adFB6741fa" }, "deployer": "0xbe6b2860d3c17a719be0A4911EA0EE689e8357f3", "usdt": { @@ -68,7 +92,8 @@ "zksync-goerli": "0xb5372ed3bb2CbA63e7908066ac10ee94d30eA839", "base-goerli": "0x876A4f6eCF13EEb101F9E75FCeF58f19Ff383eEB", "sepolia": "0x876A4f6eCF13EEb101F9E75FCeF58f19Ff383eEB", - "scroll-sepolia": "0x9C80EdD342b5D179c3a87946fC1F0963BfcaAa09" + "scroll-sepolia": "0x9C80EdD342b5D179c3a87946fC1F0963BfcaAa09", + "arbitrum-sepolia": "0x3b8Bb7348D4F581e67E2498574F73e4B9Fc51855" }, "usdc": { "goerli": "0xe9784E0d9A939dbe966b021DE3cd877284DB1B99", diff --git a/helix-contract/contracts/ln/base/LnBridgeSourceV3.sol b/helix-contract/contracts/ln/base/LnBridgeSourceV3.sol index 05239c1b..87b6a38e 100644 --- a/helix-contract/contracts/ln/base/LnBridgeSourceV3.sol +++ b/helix-contract/contracts/ln/base/LnBridgeSourceV3.sol @@ -9,7 +9,8 @@ import "../../utils/TokenTransferHelper.sol"; /// @notice LnBridgeSourceV3 is a contract to help user lock token and then trigger remote chain relay /// @dev See https://github.com/helix-bridge/contracts/tree/master/helix-contract contract LnBridgeSourceV3 is Pausable, AccessController { - uint256 constant public SLASH_EXPIRE_TIME = 60 * 60; + uint256 constant public LOCK_TIME_DISTANCE = 15 minutes; + uint256 constant public SLASH_EXPIRE_TIME = 1 hours; uint256 constant public MAX_TRANSFER_AMOUNT = type(uint112).max; // liquidity fee base rate // liquidityFee = liquidityFeeRate / LIQUIDITY_FEE_RATE_BASE * sendAmount @@ -46,7 +47,10 @@ contract LnBridgeSourceV3 is Pausable, AccessController { uint112 totalFee; uint112 amount; address receiver; - uint256 nonce; + // use this timestamp as the lock time + // can't be too far from the block that the transaction confirmed + // This timestamp can also be adjusted to produce different transferId + uint256 timestamp; } // hash(remoteChainId, sourceToken, targetToken) => TokenInfo mapping(bytes32=>TokenInfo) public tokenInfos; @@ -62,7 +66,6 @@ contract LnBridgeSourceV3 is Pausable, AccessController { // when we wan't to get tokenInfo from lockInfo, we should get the key(bytes32) from tokenIndex, then get tokenInfo from key struct LockInfo { uint112 amountWithFeeAndPenalty; - uint64 timestamp; uint32 tokenIndex; uint8 status; } @@ -100,8 +103,7 @@ contract LnBridgeSourceV3 is Pausable, AccessController { TransferParams params, bytes32 transferId, uint112 targetAmount, - uint112 fee, - uint64 timestamp + uint112 fee ); event LnProviderUpdated( uint256 remoteChainId, @@ -115,7 +117,7 @@ contract LnBridgeSourceV3 is Pausable, AccessController { event PenaltyReserveUpdated(address provider, address sourceToken, uint256 updatedPanaltyReserve); event LiquidityWithdrawn(bytes32[] transferIds, address provider, uint256 amount); event TransferSlashed(bytes32 transferId, address provider, address slasher, uint112 slashAmount); - event LnProviderPaused(address provider, uint256 remoteChainId, address sourceToken, address targetToken, bool paused); + event LnProviderPaused(address provider, uint256 remoteChainId, address sourceToken, address targetToken, bool paused); modifier allowRemoteCall(uint256 _remoteChainId) { _verifyRemote(_remoteChainId); @@ -320,6 +322,12 @@ contract LnBridgeSourceV3 is Pausable, AccessController { } function lockAndRemoteRelease(TransferParams calldata _params) whenNotPaused external payable { + // timestamp must be close to the block time + require( + _params.timestamp >= block.timestamp - LOCK_TIME_DISTANCE && _params.timestamp <= block.timestamp + LOCK_TIME_DISTANCE, + "timestamp is too far from block time" + ); + // check transfer info bytes32 tokenKey = getTokenKey(_params.remoteChainId, _params.sourceToken, _params.targetToken); TokenInfo memory tokenInfo = tokenInfos[tokenKey]; @@ -344,8 +352,8 @@ contract LnBridgeSourceV3 is Pausable, AccessController { require(remoteAmount < MAX_TRANSFER_AMOUNT && remoteAmount > 0, "overflow amount"); bytes32 transferId = getTransferId(_params, uint112(remoteAmount)); require(lockInfos[transferId].status == 0, "transferId exist"); - lockInfos[transferId] = LockInfo(amountWithFeeAndPenalty, uint64(block.timestamp), tokenInfo.index, LOCK_STATUS_LOCKED); - emit TokenLocked(_params, transferId, uint112(remoteAmount), uint112(providerFee), uint64(block.timestamp)); + lockInfos[transferId] = LockInfo(amountWithFeeAndPenalty, tokenInfo.index, LOCK_STATUS_LOCKED); + emit TokenLocked(_params, transferId, uint112(remoteAmount), uint112(providerFee)); // update protocol fee income // leave the protocol fee into contract, and admin can withdraw this fee anytime @@ -413,40 +421,27 @@ contract LnBridgeSourceV3 is Pausable, AccessController { uint256 _remoteChainId, bytes32 _transferId, // slasher, amount and lnProvider is verified on the target chain - uint112 _sourceAmount, address _lnProvider, - uint64 _timestamp, address _slasher ) external allowRemoteCall(_remoteChainId) { LockInfo memory lockInfo = lockInfos[_transferId]; require(lockInfo.status == LOCK_STATUS_LOCKED, "invalid lock status"); - require(lockInfo.amountWithFeeAndPenalty >= _sourceAmount, "invalid amount"); bytes32 tokenKey = tokenIndexer[lockInfo.tokenIndex]; TokenInfo memory tokenInfo = tokenInfos[tokenKey]; lockInfos[_transferId].status = LOCK_STATUS_SLASHED; - uint112 slashAmount = _sourceAmount; - // recheck the timestamp - // expired - if (_timestamp == lockInfo.timestamp && lockInfo.timestamp + SLASH_EXPIRE_TIME < block.timestamp) { - slashAmount = lockInfo.amountWithFeeAndPenalty; - // pause this provider if slashed - bytes32 providerKey = getProviderKey(_remoteChainId, _lnProvider, tokenInfo.sourceToken, tokenInfo.targetToken); - srcProviders[providerKey].pause = true; - emit LnProviderPaused(_lnProvider, _remoteChainId, tokenInfo.sourceToken, tokenInfo.targetToken, true); - } else { - // this means the slasher help the provider relay message and get no reward - // redeposit penalty(the fee and penalty) for lnProvider - bytes32 key = getProviderStateKey(tokenInfo.sourceToken, _lnProvider); - penaltyReserves[key] = penaltyReserves[key] + lockInfo.amountWithFeeAndPenalty - slashAmount; - } - // transfer slashAmount to slasher + // pause this provider if slashed + bytes32 providerKey = getProviderKey(_remoteChainId, _lnProvider, tokenInfo.sourceToken, tokenInfo.targetToken); + srcProviders[providerKey].pause = true; + emit LnProviderPaused(_lnProvider, _remoteChainId, tokenInfo.sourceToken, tokenInfo.targetToken, true); + + // transfer token to slasher if (tokenInfo.sourceToken == address(0)) { - TokenTransferHelper.safeTransferNative(_slasher, slashAmount); + TokenTransferHelper.safeTransferNative(_slasher, lockInfo.amountWithFeeAndPenalty); } else { - TokenTransferHelper.safeTransfer(tokenInfo.sourceToken, _slasher, slashAmount); + TokenTransferHelper.safeTransfer(tokenInfo.sourceToken, _slasher, lockInfo.amountWithFeeAndPenalty); } - emit TransferSlashed(_transferId, _lnProvider, _slasher, slashAmount); + emit TransferSlashed(_transferId, _lnProvider, _slasher, lockInfo.amountWithFeeAndPenalty); } function getProviderKey(uint256 _remoteChainId, address _provider, address _sourceToken, address _targetToken) pure public returns(bytes32) { @@ -474,7 +469,7 @@ contract LnBridgeSourceV3 is Pausable, AccessController { _params.receiver, _params.amount, _remoteAmount, - _params.nonce + _params.timestamp )); } diff --git a/helix-contract/contracts/ln/base/LnBridgeTargetV3.sol b/helix-contract/contracts/ln/base/LnBridgeTargetV3.sol index 834e939f..1d6213b4 100644 --- a/helix-contract/contracts/ln/base/LnBridgeTargetV3.sol +++ b/helix-contract/contracts/ln/base/LnBridgeTargetV3.sol @@ -19,8 +19,6 @@ contract LnBridgeTargetV3 { // sourceAmount: the send amount on source chain struct SlashInfo { uint256 remoteChainId; - uint64 lockTimestamp; - uint112 sourceAmount; address slasher; } @@ -32,7 +30,7 @@ contract LnBridgeTargetV3 { uint112 sourceAmount; uint112 targetAmount; address receiver; - uint256 nonce; + uint256 timestamp; } // transferId => FillTransfer @@ -65,7 +63,7 @@ contract LnBridgeTargetV3 { _params.receiver, _params.sourceAmount, _params.targetAmount, - _params.nonce + _params.timestamp )); require(_expectedTransferId == transferId, "check expected transferId failed"); FillTransfer memory fillTransfer = fillTransfers[transferId]; @@ -89,7 +87,6 @@ contract LnBridgeTargetV3 { // 3. send a cross-chain message to source chain to withdraw the amount, fee and penalty from lnProvider function requestSlashAndRemoteRelease( RelayParams calldata _params, - uint64 _timestamp, bytes32 _expectedTransferId, uint256 _feePrepaid, bytes memory _extParams @@ -103,7 +100,7 @@ contract LnBridgeTargetV3 { _params.receiver, _params.sourceAmount, _params.targetAmount, - _params.nonce + _params.timestamp )); require(_expectedTransferId == transferId, "check expected transferId failed"); @@ -112,9 +109,9 @@ contract LnBridgeTargetV3 { // suppose source chain and target chain has the same block timestamp // event the timestamp is not sync exactly, this TIMEOUT is also verified on source chain - require(_timestamp < block.timestamp - LnBridgeHelper.SLASH_EXPIRE_TIME, "time not expired"); + require(_params.timestamp < block.timestamp - LnBridgeHelper.SLASH_EXPIRE_TIME, "time not expired"); fillTransfers[transferId] = FillTransfer(uint64(block.timestamp), _params.provider); - slashInfos[transferId] = SlashInfo(_params.remoteChainId, _timestamp, _params.sourceAmount, msg.sender); + slashInfos[transferId] = SlashInfo(_params.remoteChainId, msg.sender); if (_params.targetToken == address(0)) { require(msg.value == _params.targetAmount + _feePrepaid, "invalid value"); @@ -127,9 +124,7 @@ contract LnBridgeTargetV3 { ILnBridgeSourceV3.slash.selector, block.chainid, transferId, - _params.sourceAmount, _params.provider, - _timestamp, msg.sender ); _sendMessageToSource(_params.remoteChainId, message, _feePrepaid, _extParams); @@ -148,9 +143,7 @@ contract LnBridgeTargetV3 { ILnBridgeSourceV3.slash.selector, block.chainid, transferId, - slashInfo.sourceAmount, fillTransfer.provider, - slashInfo.lockTimestamp, slashInfo.slasher ); _sendMessageToSource(slashInfo.remoteChainId, message, msg.value, _extParams); diff --git a/helix-contract/contracts/ln/interface/ILnBridgeSourceV3.sol b/helix-contract/contracts/ln/interface/ILnBridgeSourceV3.sol index a770f586..0e2ca4aa 100644 --- a/helix-contract/contracts/ln/interface/ILnBridgeSourceV3.sol +++ b/helix-contract/contracts/ln/interface/ILnBridgeSourceV3.sol @@ -5,9 +5,7 @@ interface ILnBridgeSourceV3 { function slash( uint256 _remoteChainId, bytes32 _transferId, - uint112 _sourceAmount, address _lnProvider, - uint64 _timestamp, address _slasher ) external; function withdrawLiquidity( diff --git a/helix-contract/deploy/create2.js b/helix-contract/deploy/create2.js index 98d8262e..677a4f9b 100644 --- a/helix-contract/deploy/create2.js +++ b/helix-contract/deploy/create2.js @@ -6,10 +6,10 @@ var Create2 = { const bytecode = `${factory.bytecode}${encodedParams.slice(2)}`; return bytecode; }, - deploy: async function(deployAddress, wallet, bytecode, salt) { + deploy: async function(deployAddress, wallet, bytecode, salt, gasLimit) { const hexSalt = ethers.utils.id(salt.toString()); const deployer = await ethers.getContractAt("Create2Deployer", deployAddress, wallet); - const result = await (await deployer.deploy(bytecode, hexSalt)).wait(); + const result = await (await deployer.deploy(bytecode, hexSalt, {gasLimit: gasLimit})).wait(); const targetEvent = result.events.find((e) => e.event == 'Deployed'); const abiCoder = ethers.utils.defaultAbiCoder; const eventParams = abiCoder.decode(["address", "uint256"], targetEvent.data); diff --git a/helix-contract/deploy/deploy_ln_create2.js b/helix-contract/deploy/deploy_ln_create2.js index bc7bbf10..f7dd052d 100644 --- a/helix-contract/deploy/deploy_ln_create2.js +++ b/helix-contract/deploy/deploy_ln_create2.js @@ -1,6 +1,7 @@ const ethUtil = require('ethereumjs-util'); const abi = require('ethereumjs-abi'); const secp256k1 = require('secp256k1'); +const fs = require("fs"); var Create2 = require("./create2.js"); @@ -21,8 +22,9 @@ async function deployCreate2Deployer(networkUrl, version) { from: w.address, to: "0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7", data: `${salt}${bytecode.slice(2)}`, - gasPrice: 2100000000, - nonce: 0, + gasLimit: 2000000, + //gasPrice: 2100000000, + //nonce: 0, }; const tx = await w.sendTransaction(unsignedTransaction); console.log(`deploy create2 tx: ${tx.hash}, salt: ${salt}`); @@ -31,17 +33,13 @@ async function deployCreate2Deployer(networkUrl, version) { // 2. deploy mapping token factory async function main() { - //await deployCreate2Deployer('https://rpc.ankr.com/eth_goerli', 'v1.0.0'); - //await deployCreate2Deployer('https://goerli-rollup.arbitrum.io/rpc', 'v1.0.0'); - //await deployCreate2Deployer('https://rpc.testnet.mantle.xyz', 'v1.0.0'); - //await deployCreate2Deployer('https://rpc.goerli.linea.build', 'v1.0.0'); - //await deployCreate2Deployer('https://arb1.arbitrum.io/rpc', 'v1.0.0'); - //await deployCreate2Deployer('https://rpc.mantle.xyz', 'v1.0.0'); - //await deployCreate2Deployer('https://crab-rpc.darwinia.network', 'v1.0.0'); - //await deployCreate2Deployer('https://binance.llamarpc.com', 'v1.0.0'); - //await deployCreate2Deployer('https://mainnet.base.org', 'v1.0.0'); - //await deployCreate2Deployer('https://optimism.llamarpc.com', 'v1.0.0'); - await deployCreate2Deployer('https://rpc.linea.build', 'v1.0.0'); + const pathConfig = "./address/ln-dev.json"; + const configure = JSON.parse( + fs.readFileSync(pathConfig, "utf8") + ); + + const network = configure.chains['arbitrum-sepolia']; + await deployCreate2Deployer(network.url, 'v1.0.0'); } main() diff --git a/helix-contract/deploy/deploy_ln_messager.js b/helix-contract/deploy/deploy_ln_messager.js index 711edf41..b6de9cbb 100644 --- a/helix-contract/deploy/deploy_ln_messager.js +++ b/helix-contract/deploy/deploy_ln_messager.js @@ -1,6 +1,7 @@ const ethUtil = require('ethereumjs-util'); const abi = require('ethereumjs-abi'); const secp256k1 = require('secp256k1'); +const fs = require("fs"); var ProxyDeployer = require("./proxy.js"); @@ -78,10 +79,10 @@ const arbitrumSepoliaNetwork = { chainId: 421614, msglineChainId: 421614, msglineAddress: "0x000C61ca18583C9504691f43Ea43C2c638772487", + lzChainId: 10231, + endpoint: "0x6098e96a28E02f27B1e6BD381f870F1C8Bd169d3", }; - - const scrollNetwork = { url: "https://sepolia-rpc.scroll.io/", dao: "0x88a39B052d477CfdE47600a7C9950a441Ce61cb4", @@ -94,6 +95,8 @@ const sepoliaNetwork = { dao: "0x88a39B052d477CfdE47600a7C9950a441Ce61cb4", chainId: 11155111, scrollMessager: "0x50c7d3e7f7c656493D1D76aaa1a836CedfCBB16A", + lzChainId: 10161, + endpoint: "0xae92d5aD7583AD66E49A0c67BAd18F6ba52dDDc1", } function wait(ms) { @@ -126,8 +129,8 @@ async function main() { const scrollWallet = wallet(scrollNetwork.url); const sepoliaWallet = wallet(sepoliaNetwork.url); - /* // deploy arb<>eth + /* console.log("deploy arb <> eth messager"); const Eth2ArbReceiveService = await deployContract(arbWallet, "Eth2ArbReceiveService", arbitrumNetwork.dao, goerliNetwork.chainId); const Eth2ArbSendService = await deployContract(goerliWallet, "Eth2ArbSendService", goerliNetwork.dao, goerliNetwork.inbox, arbitrumNetwork.chainId); @@ -143,15 +146,23 @@ async function main() { await Eth2LineaReceiveService.setRemoteMessager(Eth2LineaSendService.address); await Eth2LineaSendService.setRemoteMessager(Eth2LineaReceiveService.address); await wait(10000); + */ // deploy layerZero - console.log("deploy layerzero messager"); - const lzGoerli = await deployContract(goerliWallet, "LayerZeroMessager", goerliNetwork.dao, goerliNetwork.endpoint); - const lzArbitrum = await deployContract(arbWallet, "LayerZeroMessager", arbitrumNetwork.dao, arbitrumNetwork.endpoint); - const lzLinea = await deployContract(lineaWallet, "LayerZeroMessager", lineaNetwork.dao, lineaNetwork.endpoint); - const lzMantle = await deployContract(mantleWallet, "LayerZeroMessager", mantleNetwork.dao, mantleNetwork.endpoint); - // zkSync has been deployed - const lzZkSync = await ethers.getContractAt("LayerZeroMessager", zkSyncNetwork.layerzeroMessager, zkSyncWallet); + //console.log("deploy layerzero messager"); + //const lzGoerli = await deployContract(goerliWallet, "LayerZeroMessager", goerliNetwork.dao, goerliNetwork.endpoint); + //const lzArbitrum = await deployContract(arbWallet, "LayerZeroMessager", arbitrumNetwork.dao, arbitrumNetwork.endpoint); + //const lzLinea = await deployContract(lineaWallet, "LayerZeroMessager", lineaNetwork.dao, lineaNetwork.endpoint); + //const lzMantle = await deployContract(mantleWallet, "LayerZeroMessager", mantleNetwork.dao, mantleNetwork.endpoint); + //// zkSync has been deployed + //const lzZkSync = await ethers.getContractAt("LayerZeroMessager", zkSyncNetwork.layerzeroMessager, zkSyncWallet); + + const lzSepolia = await deployContract(sepoliaWallet, "LayerZeroMessager", sepoliaNetwork.dao, sepoliaNetwork.endpoint); + const lzArbitrumSepolia = await deployContract(arbSepoliaWallet, "LayerZeroMessager", arbitrumSepoliaNetwork.dao, arbitrumSepoliaNetwork.endpoint); + await lzSepolia.setRemoteMessager(arbitrumSepoliaNetwork.chainId, arbitrumSepoliaNetwork.lzChainId, lzArbitrumSepolia.address); + await lzArbitrumSepolia.setRemoteMessager(sepoliaNetwork.chainId, sepoliaNetwork.lzChainId, lzSepolia.address); + console.log("confgure layerzero messager"); + /* await lzGoerli.setRemoteMessager(arbitrumNetwork.chainId, arbitrumNetwork.lzChainId, lzArbitrum.address); await lzLinea.setRemoteMessager(arbitrumNetwork.chainId, arbitrumNetwork.lzChainId, lzArbitrum.address); await lzMantle.setRemoteMessager(arbitrumNetwork.chainId, arbitrumNetwork.lzChainId, lzArbitrum.address); @@ -176,7 +187,9 @@ async function main() { await lzLinea.setRemoteMessager(goerliNetwork.chainId, goerliNetwork.lzChainId, lzGoerli.address); await lzMantle.setRemoteMessager(goerliNetwork.chainId, goerliNetwork.lzChainId, lzGoerli.address); await lzZkSync.setRemoteMessager(goerliNetwork.chainId, goerliNetwork.lzChainId, lzGoerli.address); + */ // deploy axelar + /* console.log("deploy axelar messager"); const axGoerli = await deployContract(goerliWallet, "AxelarMessager", goerliNetwork.dao, goerliNetwork.axGateway, goerliNetwork.axGasService); const axArbitrum = await deployContract(arbWallet, "AxelarMessager", arbitrumNetwork.dao, arbitrumNetwork.axGateway, arbitrumNetwork.axGasService); @@ -200,6 +213,7 @@ async function main() { */ // deploy crab<>arbitrum sepolia + /* const msglineCrab = await deployContract(crabWallet, "DarwiniaMsglineMessager", crabNetwork.dao, arbitrumSepoliaNetwork.msglineAddress); const msglineArbSepolia = await deployContract(arbSepoliaWallet, "DarwiniaMsglineMessager", arbitrumSepoliaNetwork.dao, crabNetwork.msglineAddress); await msglineCrab.setRemoteMessager(arbitrumSepoliaNetwork.chainId, arbitrumSepoliaNetwork.msglineChainId, msglineArbSepolia.address); @@ -213,6 +227,7 @@ async function main() { await Eth2ScrollReceiveService.setRemoteMessager(Eth2ScrollSendService.address); await Eth2ScrollSendService.setRemoteMessager(Eth2ScrollReceiveService.address); await wait(10000); + */ /* // deploy debug messager await deployContract(goerliWallet, "DebugMessager"); diff --git a/helix-contract/deploy/deploy_ln_test_token.js b/helix-contract/deploy/deploy_ln_test_token.js index 927fb63a..5aa20d32 100644 --- a/helix-contract/deploy/deploy_ln_test_token.js +++ b/helix-contract/deploy/deploy_ln_test_token.js @@ -86,6 +86,11 @@ const arbitrumSepoliaNetwork = { name: "Helix Test Token USDC", symbol: "USDC", decimals: 18 + }, + { + name: "Helix Test Token USDT", + symbol: "USDT", + decimals: 18 } ] }; @@ -130,6 +135,14 @@ function wallet(url) { // 2. deploy mapping token factory async function main() { + const w = wallet(arbitrumSepoliaNetwork.url); + const tokenInfo = arbitrumSepoliaNetwork.tokens[1]; + const tokenContract = await ethers.getContractFactory("HelixTestErc20", w); + const token = await tokenContract.deploy(tokenInfo.name, tokenInfo.symbol, tokenInfo.decimals); + await token.deployed(); + console.log(`finish to deploy test token contract, network is: ${network.url}, address is: ${token.address}`); + return; + const networks = [goerliNetwork, mantleNetwork, arbitrumNetwork, lineaNetwork, sepoliaNetwork, scrollSepoliaNetwork]; for (const network of networks) { const w = wallet(network.url); diff --git a/helix-contract/deploy/deploy_lnv3_configure.js b/helix-contract/deploy/deploy_lnv3_configure.js index b6eeef38..4ece6dab 100644 --- a/helix-contract/deploy/deploy_lnv3_configure.js +++ b/helix-contract/deploy/deploy_lnv3_configure.js @@ -10,21 +10,6 @@ const privateKey = process.env.PRIKEY const kNativeTokenAddress = "0x0000000000000000000000000000000000000000"; const relayer = "0xB2a0654C6b2D0975846968D5a3e729F5006c2894"; -const goerliNetwork = { - name: "goerli", - url: "https://rpc.ankr.com/eth_goerli", - chainId: 5, - eth: "0x0000000000000000000000000000000000000000", - mnt: "0xc1dC2d65A2243c22344E725677A3E3BEBD26E604", -}; - -const zkSyncGoerliNetwork = { - name: "zksync-goerli", - url: "https://zksync2-testnet.zksync.dev", - chainId: 280, - eth: "0x0000000000000000000000000000000000000000", -}; - function wait(ms) { return new Promise(resolve => setTimeout(() => resolve(), ms)); }; @@ -35,7 +20,11 @@ function wallet(url) { return wallet; } -async function connectUsingLayerzero(configure, leftWallet, rightWallet, leftNetwork, rightNetwork) { +async function connectUsingLayerzero(configure, pair) { + const leftNetwork = pair.networks[0]; + const rightNetwork = pair.networks[1]; + const leftWallet = pair.wallets[0]; + const rightWallet = pair.wallets[1]; const leftMessagerAddess = configure.messagers[leftNetwork.name].layerzeroMessager; const rightMessagerAddress = configure.messagers[rightNetwork.name].layerzeroMessager; const leftBridgeProxy = leftNetwork.chainId === 280 ? configure.LnV3BridgeProxy.zkSync : configure.LnV3BridgeProxy.others; @@ -54,19 +43,15 @@ async function connectUsingLayerzero(configure, leftWallet, rightWallet, leftNet await right.setSendService(leftNetwork.chainId, left.address, rightMessagerAddress); } -async function connectAll(configure, goerliWallet, zkSyncWallet) { - await connectUsingLayerzero(configure, zkSyncWallet, goerliWallet, zkSyncGoerliNetwork, goerliNetwork); -} - async function registerToken(configure, srcWallet, dstWallet, srcNetwork, dstNetwork, srcToken, dstToken, tokenIndex) { let srcDecimals = 18; let dstDecimals = 18; - let srcTokenAddress = srcNetwork[srcToken]; - let dstTokenAddress = dstNetwork[dstToken]; - if (srcToken !== 'eth' && srcToken !== 'mnt') { + let srcTokenAddress = kNativeTokenAddress; + let dstTokenAddress = kNativeTokenAddress; + if (srcToken !== 'eth') { srcTokenAddress = configure[srcToken][srcNetwork.name]; } - if (dstToken !== 'eth' && dstToken !== 'mnt') { + if (dstToken !== 'eth') { dstTokenAddress = configure[dstToken][dstNetwork.name]; } if (srcTokenAddress != kNativeTokenAddress) { @@ -100,26 +85,31 @@ async function registerToken(configure, srcWallet, dstWallet, srcNetwork, dstNet console.log(`finished register token bridge: ${srcNetwork.chainId}->${dstNetwork.chainId}, ${srcToken}->${dstToken}`); } -async function registerAllToken(configure, goerliWallet, zkSyncWallet) { +async function registerAllToken(configure, pair) { + const leftNetwork = pair.networks[0]; + const rightNetwork = pair.networks[1]; + const leftWallet = pair.wallets[0]; + const rightWallet = pair.wallets[1]; // zkSync<>eth - let tokenIndex = 1; - await registerToken(configure, zkSyncWallet, goerliWallet, zkSyncGoerliNetwork, goerliNetwork, "usdc", "usdc", tokenIndex++); - await registerToken(configure, goerliWallet, zkSyncWallet, goerliNetwork, zkSyncGoerliNetwork, "usdc", "usdc", tokenIndex++); - await registerToken(configure, zkSyncWallet, goerliWallet, zkSyncGoerliNetwork, goerliNetwork, "usdt", "usdt", tokenIndex++); - await registerToken(configure, goerliWallet, zkSyncWallet, goerliNetwork, zkSyncGoerliNetwork, "usdt", "usdt", tokenIndex++); - await registerToken(configure, zkSyncWallet, goerliWallet, zkSyncGoerliNetwork, goerliNetwork, "eth", "eth", tokenIndex++); - await registerToken(configure, goerliWallet, zkSyncWallet, goerliNetwork, zkSyncGoerliNetwork, "eth", "eth", tokenIndex++); + let leftTokenIndex = 1; + let rightTokenIndex = 1; + await registerToken(configure, leftWallet, rightWallet, leftNetwork, rightNetwork, "usdc", "usdc", leftTokenIndex++); + await registerToken(configure, rightWallet, leftWallet, rightNetwork, leftNetwork, "usdc", "usdc", rightTokenIndex++); + await registerToken(configure, leftWallet, rightWallet, leftNetwork, rightNetwork, "usdt", "usdt", leftTokenIndex++); + await registerToken(configure, rightWallet, leftWallet, rightNetwork, leftNetwork, "usdt", "usdt", rightTokenIndex++); + await registerToken(configure, leftWallet, rightWallet, leftNetwork, rightNetwork, "eth", "eth", leftTokenIndex++); + await registerToken(configure, rightWallet, leftWallet, rightNetwork, leftNetwork, "eth", "eth", rightTokenIndex++); } async function registerRelayer(configure, srcWallet, dstWallet, srcNetwork, dstNetwork, srcToken, dstToken) { - let srcTokenAddress = srcNetwork[srcToken]; - let dstTokenAddress = dstNetwork[dstToken]; + let srcTokenAddress = kNativeTokenAddress; + let dstTokenAddress = kNativeTokenAddress; let srcDecimals = 18; let dstDecimals = 18; - if (srcToken !== 'eth' && srcToken !== 'mnt') { + if (srcToken !== 'eth') { srcTokenAddress = configure[srcToken][srcNetwork.name]; } - if (dstToken !== 'eth' && dstToken !== 'mnt') { + if (dstToken !== 'eth') { dstTokenAddress = configure[dstToken][dstNetwork.name]; } @@ -142,7 +132,7 @@ async function registerRelayer(configure, srcWallet, dstWallet, srcNetwork, dstN let penalty = ethers.utils.parseUnits("100000", srcDecimals); let value = 0; if (dstTokenAddress == kNativeTokenAddress) { - penalty = ethers.utils.parseUnits("0.1", dstDecimals); + penalty = ethers.utils.parseUnits("0.01", dstDecimals); value = penalty; } const source = await ethers.getContractAt("HelixLnBridgeV3", proxyAddress, srcWallet); @@ -154,24 +144,32 @@ async function registerRelayer(configure, srcWallet, dstWallet, srcNetwork, dstN baseFee, liquidityFeeRate, ethers.utils.parseUnits("1000000", srcDecimals), + { gasLimit: 2000000 } ); await source.depositPenaltyReserve( srcTokenAddress, penalty, - { value: value}, + { + value: value, + gasLimit: 2000000 + } ); console.log(`finished register relayer: ${srcNetwork.chainId}->${dstNetwork.chainId}, ${srcToken}->${dstToken}`); } -async function registerAllRelayer(configure, goerliWallet, zkSyncWallet) { +async function registerAllRelayer(configure, pair) { + const leftNetwork = pair.networks[0]; + const rightNetwork = pair.networks[1]; + const leftWallet = pair.wallets[0]; + const rightWallet = pair.wallets[1]; // eth<>zkSync - await registerRelayer(configure, goerliWallet, zkSyncWallet, goerliNetwork, zkSyncGoerliNetwork, "usdc", "usdc"); - await registerRelayer(configure, zkSyncWallet, goerliWallet, zkSyncGoerliNetwork, goerliNetwork, "usdc", "usdc"); - await registerRelayer(configure, goerliWallet, zkSyncWallet, goerliNetwork, zkSyncGoerliNetwork, "usdt", "usdt"); - await registerRelayer(configure, zkSyncWallet, goerliWallet, zkSyncGoerliNetwork, goerliNetwork, "usdt", "usdt"); - await registerRelayer(configure, goerliWallet, zkSyncWallet, goerliNetwork, zkSyncGoerliNetwork, "eth", "eth"); - await registerRelayer(configure, zkSyncWallet, goerliWallet, zkSyncGoerliNetwork, goerliNetwork, "eth", "eth"); + await registerRelayer(configure, leftWallet, rightWallet, leftNetwork, rightNetwork, "usdc", "usdc"); + await registerRelayer(configure, rightWallet, leftWallet, rightNetwork, leftNetwork, "usdc", "usdc"); + await registerRelayer(configure, leftWallet, rightWallet, leftNetwork, rightNetwork, "usdt", "usdt"); + await registerRelayer(configure, rightWallet, leftWallet, rightNetwork, leftNetwork, "usdt", "usdt"); + await registerRelayer(configure, leftWallet, rightWallet, leftNetwork, rightNetwork, "eth", "eth"); + await registerRelayer(configure, rightWallet, leftWallet, rightNetwork, leftNetwork, "eth", "eth"); } async function mintToken(configure, tokenSymbol, network, wallet, to) { @@ -191,16 +189,14 @@ async function approveToken(configure, tokenSymbol, network, wallet) { const proxyAddress = network.chainId === 280 ? configure.LnV3BridgeProxy.zkSync : configure.LnV3BridgeProxy.others; - await token.approve(proxyAddress, ethers.utils.parseUnits("10000000000000", decimals)); + await token.approve(proxyAddress, ethers.utils.parseUnits("10000000000000", decimals), {gasLimit: 1000000}); await wait(5000); console.log("finished to approve", tokenSymbol); } -async function approveAll(configure, goerliWallet, zkSyncWallet) { - await approveToken(configure, "usdc", goerliNetwork, goerliWallet); - await approveToken(configure, "usdt", goerliNetwork, goerliWallet); - await approveToken(configure, "usdt", zkSyncGoerliNetwork, zkSyncWallet); - await approveToken(configure, "usdc", zkSyncGoerliNetwork, zkSyncWallet); +async function approveAll(configure, network, wallet) { + await approveToken(configure, "usdc", network, wallet); + await approveToken(configure, "usdt", network, wallet); } // 2. deploy mapping token factory @@ -210,15 +206,27 @@ async function main() { fs.readFileSync(pathConfig, "utf8") ); - const goerliWallet = wallet(goerliNetwork.url); - const zkSyncWallet = wallet(zkSyncGoerliNetwork.url); + const network01 = configure.chains['sepolia']; + const network02 = configure.chains['arbitrum-sepolia']; + + const wallet01 = wallet(network01.url); + const wallet02 = wallet(network02.url); + + const pair = { + networks: [network01, network02], + wallets: [wallet01, wallet02] + }; + + // connect + //await connectUsingLayerzero(configure, pair); + // register tokens + //await registerAllToken(configure, pair); + // approve + //await approveAll(configure, network01, wallet01); + //await approveAll(configure, network02, wallet02); - // set messager service - //await connectAll(configure, goerliWallet, zkSyncWallet); - //await registerAllToken(configure, goerliWallet, zkSyncWallet); - //await mintAll(configure, relayer, arbWallet, lineaWallet, goerliWallet, mantleWallet, zkSyncWallet, crabWallet, arbSepoliaWallet); - //await approveAll(configure, goerliWallet, zkSyncWallet); - await registerAllRelayer(configure, goerliWallet, zkSyncWallet); + //await mintToken(configure, 'usdc', network01, wallet01, '0xB2a0654C6b2D0975846968D5a3e729F5006c2894'); + await registerAllRelayer(configure, pair); console.log("finished!"); } diff --git a/helix-contract/deploy/deploy_lnv3_logic.js b/helix-contract/deploy/deploy_lnv3_logic.js index 5c37300d..4bdd1782 100644 --- a/helix-contract/deploy/deploy_lnv3_logic.js +++ b/helix-contract/deploy/deploy_lnv3_logic.js @@ -1,35 +1,40 @@ const ethUtil = require('ethereumjs-util'); const abi = require('ethereumjs-abi'); const secp256k1 = require('secp256k1'); +const fs = require("fs"); var Create2 = require("./create2.js"); const privateKey = process.env.PRIKEY -const goerliNetwork = { - url: "https://rpc.ankr.com/eth_goerli", - deployer: "0xbe6b2860d3c17a719be0A4911EA0EE689e8357f3", -}; - -function wallet(url) { - const provider = new ethers.providers.JsonRpcProvider(url); +function wallet(configure, network) { + const provider = new ethers.providers.JsonRpcProvider(network.url); const wallet = new ethers.Wallet(privateKey, provider); return wallet; } +function chainInfo(configure, network) { + return configure.chains[network]; +} + async function deployLnBridgeV3(wallet, deployerAddress, salt) { const bridgeContract = await ethers.getContractFactory("HelixLnBridgeV3", wallet); const bytecode = Create2.getDeployedBytecode(bridgeContract, [], []); - const address = await Create2.deploy(deployerAddress, wallet, bytecode, salt); + const address = await Create2.deploy(deployerAddress, wallet, bytecode, salt, 8000000); console.log("finish to deploy lnv3 bridge logic, address: ", address); return address; } // 2. deploy mapping token factory async function main() { - const w = wallet(goerliNetwork.url); - const logicAddress = await deployLnBridgeV3(w, goerliNetwork.deployer, "lnv3-logic-v1.0.0"); - console.log("finish to deploy logic contract, network is: ", goerliNetwork.url); + const pathConfig = "./address/ln-dev.json"; + const configure = JSON.parse( + fs.readFileSync(pathConfig, "utf8") + ); + + const network = chainInfo(configure, "arbitrum-sepolia"); + const w = wallet(configure, network); + const logicAddress = await deployLnBridgeV3(w, network.deployer, "lnv3-logic-v1.0.0"); } main() diff --git a/helix-contract/deploy/deploy_lnv3_proxy.js b/helix-contract/deploy/deploy_lnv3_proxy.js index 7a32c209..5cf86a33 100644 --- a/helix-contract/deploy/deploy_lnv3_proxy.js +++ b/helix-contract/deploy/deploy_lnv3_proxy.js @@ -7,12 +7,6 @@ var ProxyDeployer = require("./proxy.js"); const privateKey = process.env.PRIKEY -const goerliNetwork = { - name: "goerli", - url: "https://rpc.ankr.com/eth_goerli", - dao: "0x88a39B052d477CfdE47600a7C9950a441Ce61cb4", -}; - function wallet(url) { const provider = new ethers.providers.JsonRpcProvider(url); const wallet = new ethers.Wallet(privateKey, provider); @@ -28,7 +22,9 @@ async function deployLnBridgeV3Proxy(wallet, salt, dao, proxyAdminAddress, logic bridgeContract, logicAddress, [dao], - wallet); + wallet, + { gasLimit: 5000000 } + ); console.log("finish to deploy lnv3 bridge proxy, address:", lnBridgeProxy); return lnBridgeProxy; } @@ -40,14 +36,15 @@ async function deploy() { fs.readFileSync(pathConfig, "utf8") ); - const w = wallet(goerliNetwork.url); + const network = configure.chains['arbitrum-sepolia']; + const w = wallet(network.url); const proxyAdmin = configure.ProxyAdmin.others; const logicAddress = configure.LnV3BridgeLogic.others; const deployer = configure.deployer; let proxyAddress = await deployLnBridgeV3Proxy( w, "lnv3-v1.0.0", - goerliNetwork.dao, + network.dao, proxyAdmin, logicAddress, deployer, diff --git a/helix-contract/deploy/proxy.js b/helix-contract/deploy/proxy.js index e3ba413f..8c7b2105 100644 --- a/helix-contract/deploy/proxy.js +++ b/helix-contract/deploy/proxy.js @@ -27,13 +27,14 @@ var ProxyDeployer = { await proxy.deployed(); return proxy; }, - deployProxyContract2: async function(deployerAddress, salt, proxyAdminAdder, logicFactory, logicAddress, args, wallet) { + deployProxyContract2: async function(deployerAddress, salt, proxyAdminAdder, logicFactory, logicAddress, args, wallet, gasLimit) { const calldata = ProxyDeployer.getInitializerData(logicFactory.interface, args, "initialize"); const proxyContract = await ethers.getContractFactory("TransparentUpgradeableProxy", wallet); const deployedBytecode = Create2.getDeployedBytecode( proxyContract, ["address", "address", "bytes"], - [logicAddress, proxyAdminAdder, calldata] + [logicAddress, proxyAdminAdder, calldata], + { gasLimit: gasLimit } ); return await Create2.deploy(deployerAddress, wallet, deployedBytecode, salt); } diff --git a/helix-contract/flatten/lnv3/HelixLnBridgeV3.sol b/helix-contract/flatten/lnv3/HelixLnBridgeV3.sol index 78928016..805cae44 100644 --- a/helix-contract/flatten/lnv3/HelixLnBridgeV3.sol +++ b/helix-contract/flatten/lnv3/HelixLnBridgeV3.sol @@ -14,12 +14,12 @@ * '----------------' '----------------' '----------------' '----------------' '----------------' ' * * - * 12/28/2023 + * 1/10/2024 **/ pragma solidity ^0.8.17; -// File contracts/ln/interface/ILowLevelMessager.sol +// File contracts/interfaces/IMessager.sol // License-Identifier: MIT interface ILowLevelMessageSender { @@ -32,57 +32,21 @@ interface ILowLevelMessageReceiver { function recvMessage(address remoteSender, address localReceiver, bytes memory payload) external; } -// File contracts/ln/base/LnAccessController.sol +// File contracts/ln/interface/ILnBridgeSourceV3.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 { - address public dao; - address public operator; - address public pendingDao; - - mapping(address=>bool) public callerWhiteList; - - modifier onlyDao() { - require(msg.sender == dao, "!dao"); - _; - } - - modifier onlyOperator() { - require(msg.sender == operator, "!operator"); - _; - } - - modifier onlyWhiteListCaller() { - require(callerWhiteList[msg.sender], "caller not in white list"); - _; - } - - function _initialize(address _dao) internal { - dao = _dao; - operator = _dao; - } - - function setOperator(address _operator) onlyDao external { - operator = _operator; - } - - function authoriseAppCaller(address appAddress, bool enable) onlyDao external { - callerWhiteList[appAddress] = enable; - } - - function transferOwnership(address _dao) onlyDao external { - pendingDao = _dao; - } - - function acceptOwnership() external { - address newDao = msg.sender; - require(pendingDao == newDao, "!pendingDao"); - delete pendingDao; - dao = newDao; - } +interface ILnBridgeSourceV3 { + function slash( + uint256 _remoteChainId, + bytes32 _transferId, + address _lnProvider, + address _slasher + ) external; + function withdrawLiquidity( + bytes32[] calldata _transferIds, + uint256 _remoteChainId, + address _provider + ) external; } // File @zeppelin-solidity/contracts/token/ERC20/IERC20.sol@v4.7.3 @@ -168,10 +132,56 @@ interface IERC20 { ) external returns (bool); } -// File contracts/ln/base/TokenTransferHelper.sol +// File contracts/ln/base/LnBridgeHelper.sol // License-Identifier: MIT -library TokenTransferHelper { +library LnBridgeHelper { + // the time(seconds) for liquidity provider to delivery message + // if timeout, slasher can work. + uint256 constant public SLASH_EXPIRE_TIME = 60 * 60; + bytes32 constant public INIT_SLASH_TRANSFER_ID = bytes32(uint256(1)); + // liquidity fee base rate + // liquidityFee = liquidityFeeRate / LIQUIDITY_FEE_RATE_BASE * sendAmount + uint256 constant public LIQUIDITY_FEE_RATE_BASE = 100000; + + struct TransferParameter { + bytes32 previousTransferId; + address provider; + address sourceToken; + address targetToken; + uint112 amount; + uint256 timestamp; + address receiver; + } + + // sourceToken and targetToken is the pair of erc20 token(or native) addresses + // if sourceToken == address(0), then it's native token + // if targetToken == address(0), then remote is native token + // * `protocolFee` is the protocol fee charged by system + // * `penaltyLnCollateral` is penalty from lnProvider when the transfer slashed, if we adjust this value, it'll not affect the old transfers. + struct TokenInfo { + uint112 protocolFee; + uint112 penaltyLnCollateral; + uint8 sourceDecimals; + uint8 targetDecimals; + bool isRegistered; + } + + function sourceAmountToTargetAmount( + TokenInfo memory tokenInfo, + uint112 amount + ) internal pure returns(uint112) { + uint256 targetAmount = uint256(amount) * 10**tokenInfo.targetDecimals / 10**tokenInfo.sourceDecimals; + require(targetAmount < type(uint112).max, "overflow amount"); + return uint112(targetAmount); + } + + function calculateProviderFee(uint112 baseFee, uint16 liquidityFeeRate, uint112 amount) internal pure returns(uint112) { + uint256 fee = uint256(baseFee) + uint256(liquidityFeeRate) * uint256(amount) / LIQUIDITY_FEE_RATE_BASE; + require(fee < type(uint112).max, "overflow fee"); + return uint112(fee); + } + function safeTransfer( address token, address receiver, @@ -207,902 +217,896 @@ library TokenTransferHelper { (bool success,) = payable(receiver).call{value: amount}(""); require(success, "lnBridgeHelper:transfer native token failed"); } -} - -// 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 getProviderKey(uint256 remoteChainId, address provider, address sourceToken, address targetToken) pure internal returns(bytes32) { + return keccak256(abi.encodePacked( + remoteChainId, + provider, + sourceToken, + targetToken + )); } - function _msgData() internal view virtual returns (bytes calldata) { - return msg.data; + function getTokenKey(uint256 remoteChainId, address sourceToken, address targetToken) pure internal returns(bytes32) { + return keccak256(abi.encodePacked( + remoteChainId, + sourceToken, + targetToken + )); } } -// File @zeppelin-solidity/contracts/security/Pausable.sol@v4.7.3 +// File contracts/ln/base/LnBridgeTargetV3.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(); - _; +contract LnBridgeTargetV3 { + // timestamp: the time when transfer filled, this is also the flag that the transfer is filled(relayed or slashed) + // provider: the transfer lnProvider + struct FillTransfer { + uint64 timestamp; + address provider; } - /** - * @dev Returns true if the contract is paused, and false otherwise. - */ - function paused() public view virtual returns (bool) { - return _paused; + // lockTimestamp: the time when the transfer start from source chain + // the lockTimestamp is verified on source chain + // 1. lockTimestamp verified successed: slasher get the transfer amount, fee and penalty on source chain + // 2. lockTimestamp verified failed: slasher get the transfer amount, but the fee and penalty back to the provider + // sourceAmount: the send amount on source chain + struct SlashInfo { + uint256 remoteChainId; + address slasher; } - /** - * @dev Throws if the contract is paused. - */ - function _requireNotPaused() internal view virtual { - require(!paused(), "Pausable: paused"); + struct RelayParams { + uint256 remoteChainId; + address provider; + address sourceToken; + address targetToken; + uint112 sourceAmount; + uint112 targetAmount; + address receiver; + uint256 timestamp; } - /** - * @dev Throws if the contract is not paused. - */ - function _requirePaused() internal view virtual { - require(paused(), "Pausable: not paused"); - } + // transferId => FillTransfer + mapping(bytes32 => FillTransfer) public fillTransfers; + // transferId => SlashInfo + mapping(bytes32 => SlashInfo) public slashInfos; - /** - * @dev Triggers stopped state. - * - * Requirements: - * - * - The contract must not be paused. - */ - function _pause() internal virtual whenNotPaused { - _paused = true; - emit Paused(_msgSender()); - } + event TransferFilled(bytes32 transferId, address provider); + event SlashRequest(bytes32 transferId, uint256 remoteChainId, address provider, address sourceToken, address targetToken, address slasher); - /** - * @dev Returns to normal state. - * - * Requirements: - * - * - The contract must be paused. - */ - function _unpause() internal virtual whenPaused { - _paused = false; - emit Unpaused(_msgSender()); - } -} + function _sendMessageToSource(uint256 _remoteChainId, bytes memory _payload, uint256 feePrepaid, bytes memory _extParams) internal virtual {} -// File contracts/ln/base/LnBridgeSourceV3.sol -// License-Identifier: MIT + // relay a tx, usually called by lnProvider + // 1. update the fillTransfers storage to save the relay proof + // 2. transfer token from lnProvider to the receiver + function relay( + RelayParams calldata _params, + bytes32 _expectedTransferId, + bool _relayBySelf + ) external payable { + // _relayBySelf = true to protect that the msg.sender don't relay for others + // _relayBySelf = false to allow that lnProvider can use different account between source chain and target chain + require(!_relayBySelf || _params.provider == msg.sender, "invalid provider"); + bytes32 transferId = keccak256(abi.encodePacked( + _params.remoteChainId, + block.chainid, + _params.provider, + _params.sourceToken, + _params.targetToken, + _params.receiver, + _params.sourceAmount, + _params.targetAmount, + _params.timestamp + )); + require(_expectedTransferId == transferId, "check expected transferId failed"); + FillTransfer memory fillTransfer = fillTransfers[transferId]; + // Make sure this transfer was never filled before + require(fillTransfer.timestamp == 0, "transfer has been filled"); + fillTransfers[transferId] = FillTransfer(uint64(block.timestamp), _params.provider); + if (_params.targetToken == address(0)) { + require(msg.value == _params.targetAmount, "invalid amount"); + LnBridgeHelper.safeTransferNative(_params.receiver, _params.targetAmount); + } else { + require(msg.value == 0, "value not need"); + LnBridgeHelper.safeTransferFrom(_params.targetToken, msg.sender, _params.receiver, uint256(_params.targetAmount)); + } + emit TransferFilled(transferId, _params.provider); + } + // slash a tx when timeout + // 1. update fillTransfers and slashInfos storage to save slash proof + // 2. transfer tokens from slasher to receiver for this tx + // 3. send a cross-chain message to source chain to withdraw the amount, fee and penalty from lnProvider + function requestSlashAndRemoteRelease( + RelayParams calldata _params, + bytes32 _expectedTransferId, + uint256 _feePrepaid, + bytes memory _extParams + ) external payable { + bytes32 transferId = keccak256(abi.encodePacked( + _params.remoteChainId, + block.chainid, + _params.provider, + _params.sourceToken, + _params.targetToken, + _params.receiver, + _params.sourceAmount, + _params.targetAmount, + _params.timestamp + )); + require(_expectedTransferId == transferId, "check expected transferId failed"); -/// @title LnBridgeSourceV3 -/// @notice LnBridgeSourceV3 is a contract to help user lock token and then trigger remote chain relay -/// @dev See https://github.com/helix-bridge/contracts/tree/master/helix-contract -contract LnBridgeSourceV3 is Pausable, LnAccessController { - uint256 constant public SLASH_EXPIRE_TIME = 60 * 60; - uint256 constant public MAX_TRANSFER_AMOUNT = type(uint112).max; - // liquidity fee base rate - // liquidityFee = liquidityFeeRate / LIQUIDITY_FEE_RATE_BASE * sendAmount - uint256 constant public LIQUIDITY_FEE_RATE_BASE = 100000; - uint8 constant public LOCK_STATUS_LOCKED = 1; - uint8 constant public LOCK_STATUS_WITHDRAWN = 2; - uint8 constant public LOCK_STATUS_SLASHED = 3; - // the configure information can be updated - struct TokenConfigure { - // pay to system for each tx - uint112 protocolFee; - // Used to penalise relayer for each slashed transaction - uint112 penalty; - uint8 sourceDecimals; - uint8 targetDecimals; - } - // registered token info - struct TokenInfo { - TokenConfigure config; - // zero index is invalid - uint32 index; - address sourceToken; - address targetToken; - // accumulated system revenues - uint256 protocolFeeIncome; - } - struct TransferParams { - uint256 remoteChainId; - address provider; - address sourceToken; - address targetToken; - uint112 totalFee; - uint112 amount; - address receiver; - uint256 nonce; + FillTransfer memory fillTransfer = fillTransfers[transferId]; + require(fillTransfer.timestamp == 0, "transfer has been filled"); + + // suppose source chain and target chain has the same block timestamp + // event the timestamp is not sync exactly, this TIMEOUT is also verified on source chain + require(_params.timestamp < block.timestamp - LnBridgeHelper.SLASH_EXPIRE_TIME, "time not expired"); + fillTransfers[transferId] = FillTransfer(uint64(block.timestamp), _params.provider); + slashInfos[transferId] = SlashInfo(_params.remoteChainId, msg.sender); + + if (_params.targetToken == address(0)) { + require(msg.value == _params.targetAmount + _feePrepaid, "invalid value"); + LnBridgeHelper.safeTransferNative(_params.receiver, _params.targetAmount); + } else { + require(msg.value == _feePrepaid, "value too large"); + LnBridgeHelper.safeTransferFrom(_params.targetToken, msg.sender, _params.receiver, uint256(_params.targetAmount)); + } + bytes memory message = abi.encodeWithSelector( + ILnBridgeSourceV3.slash.selector, + block.chainid, + transferId, + _params.provider, + msg.sender + ); + _sendMessageToSource(_params.remoteChainId, message, _feePrepaid, _extParams); + emit SlashRequest(transferId, _params.remoteChainId, _params.provider, _params.sourceToken, _params.targetToken, msg.sender); } - // hash(remoteChainId, sourceToken, targetToken) => TokenInfo - mapping(bytes32=>TokenInfo) public tokenInfos; - // the token index is used to be stored in lockInfo to save gas - mapping(uint32=>bytes32) public tokenIndexer; - // amountWithFeeAndPenalty = transferAmount + providerFee + penalty < type(uint112).max - // status == 0: lockInfo not exist - // status == 1: lockInfo confirmed on source chain(has not been withdrawn or slashed) - // status == 2: lockInfo has been withdrawn - // status == 3: lockInfo has been slashed - // we don't clean lockInfo after withdraw or slash to avoid the hash collision(generate the same transferId) - struct LockInfo { - uint112 amountWithFeeAndPenalty; - uint64 timestamp; - uint32 tokenIndex; - uint8 status; + + // it's allowed to retry a slash tx because the cross-chain message may fail on source chain + // But it's required that the params must not be modified, it read from the storage saved + function retrySlash(bytes32 transferId, bytes memory _extParams) external payable { + FillTransfer memory fillTransfer = fillTransfers[transferId]; + require(fillTransfer.timestamp > 0, "transfer not filled"); + SlashInfo memory slashInfo = slashInfos[transferId]; + require(slashInfo.slasher == msg.sender, "invalid slasher"); + // send message + bytes memory message = abi.encodeWithSelector( + ILnBridgeSourceV3.slash.selector, + block.chainid, + transferId, + fillTransfer.provider, + slashInfo.slasher + ); + _sendMessageToSource(slashInfo.remoteChainId, message, msg.value, _extParams); } - // transferId => LockInfo - mapping(bytes32 => LockInfo) public lockInfos; - struct SourceProviderInfo { - uint112 baseFee; - uint16 liquidityFeeRate; - uint112 transferLimit; - bool pause; + // can't withdraw for different providers each time + // the size of the _transferIds should not be too large to be processed outof gas on source chain + function requestWithdrawLiquidity( + uint256 _remoteChainId, + bytes32[] calldata _transferIds, + address _provider, + bytes memory _extParams + ) external payable { + for (uint i = 0; i < _transferIds.length; i++) { + bytes32 transferId = _transferIds[i]; + FillTransfer memory fillTransfer = fillTransfers[transferId]; + // make sure that each transfer has the same provider + require(fillTransfer.provider == _provider, "provider invalid"); + } + bytes memory message = abi.encodeWithSelector( + ILnBridgeSourceV3.withdrawLiquidity.selector, + _transferIds, + block.chainid, + _provider + ); + _sendMessageToSource(_remoteChainId, message, msg.value, _extParams); } +} - // hash(remoteChainId, provider, sourceToken, targetToken) => SourceProviderInfo - mapping(bytes32=>SourceProviderInfo) public srcProviders; - // for a special source token, all the path start from this chain use the same panaltyReserve - // hash(sourceToken, provider) => penalty reserve - mapping(bytes32=>uint256) public penaltyReserves; +// File contracts/utils/AccessController.sol +// License-Identifier: MIT - event TokenRegistered( - bytes32 key, - uint256 remoteChainId, - address sourceToken, - address targetToken, - uint112 protocolFee, - uint112 penalty, - uint32 index - ); - event TokenInfoUpdated(bytes32 tokenInfoKey, uint112 protocolFee, uint112 penalty, uint112 sourceDecimals, uint112 targetDecimals); - event FeeIncomeClaimed(bytes32 tokenInfoKey, uint256 amount, address receiver); - event TokenLocked( - TransferParams params, - bytes32 transferId, - bytes32 idWithTimestamp, - uint112 targetAmount, - uint112 fee, - uint64 timestamp - ); - event LnProviderUpdated( - uint256 remoteChainId, - address provider, - address sourceToken, - address targetToken, - uint112 baseFee, - uint16 liquidityfeeRate, - uint112 transferLimit - ); - event PenaltyReserveUpdated(address provider, address sourceToken, uint256 updatedPanaltyReserve); - event LiquidityWithdrawn(bytes32 transferId, address provider, uint112 amount); - event TransferSlashed(bytes32 transferId, address provider, address slasher, uint112 slashAmount); - event LnProviderPaused(address provider, uint256 remoteChainId, address sourceToken, address targetToken, bool paused); +/// @title AccessController +/// @notice AccessController is a contract to control the access permission +/// @dev See https://github.com/helix-bridge/contracts/tree/master/helix-contract +contract AccessController { + address public dao; + address public operator; + address public pendingDao; - modifier allowRemoteCall(uint256 _remoteChainId) { - _verifyRemote(_remoteChainId); + modifier onlyDao() { + require(msg.sender == dao, "!dao"); _; } - function _verifyRemote(uint256 _remoteChainId) internal virtual {} + modifier onlyOperator() { + require(msg.sender == operator, "!operator"); + _; + } - function unpause() external onlyOperator { - _unpause(); + function _initialize(address _dao) internal { + dao = _dao; + operator = _dao; } - function pause() external onlyOperator { - _pause(); + function setOperator(address _operator) onlyDao external { + operator = _operator; } - function registerTokenInfo( - uint256 _remoteChainId, - address _sourceToken, - address _targetToken, - uint112 _protocolFee, - uint112 _penalty, - uint8 _sourceDecimals, - uint8 _targetDecimals, - uint32 _index - ) onlyDao external { - require(_index > 0, "invalid index"); - bytes32 key = getTokenKey(_remoteChainId, _sourceToken, _targetToken); - TokenInfo memory oldInfo = tokenInfos[key]; - require(oldInfo.index == 0, "token info exist"); - require(tokenIndexer[_index] == bytes32(0), "the index exist"); - TokenConfigure memory tokenConfig = TokenConfigure( - _protocolFee, - _penalty, - _sourceDecimals, - _targetDecimals - ); - tokenInfos[key] = TokenInfo( - tokenConfig, - _index, - _sourceToken, - _targetToken, - 0 - ); - tokenIndexer[_index] = key; - emit TokenRegistered(key, _remoteChainId, _sourceToken, _targetToken, _protocolFee, _penalty, _index); + function transferOwnership(address _dao) onlyDao external { + pendingDao = _dao; } - function updateTokenInfo( - uint256 _remoteChainId, - address _sourceToken, - address _targetToken, - uint112 _protocolFee, - uint112 _penalty, - uint8 _sourceDecimals, - uint8 _targetDecimals - ) onlyDao external { - bytes32 key = getTokenKey(_remoteChainId, _sourceToken, _targetToken); - TokenInfo memory tokenInfo = tokenInfos[key]; - require(tokenInfo.index > 0, "token not registered"); - tokenInfos[key].config = TokenConfigure( - _protocolFee, - _penalty, - _sourceDecimals, - _targetDecimals - ); - emit TokenInfoUpdated(key, _protocolFee, _penalty, _sourceDecimals, _targetDecimals); + function acceptOwnership() external { + address newDao = msg.sender; + require(pendingDao == newDao, "!pendingDao"); + delete pendingDao; + dao = newDao; } +} - // This interface should be called with exceptional caution, only when correcting registration errors, to conserve index resources. - function deleteTokenInfo(bytes32 key) onlyDao external { - TokenInfo memory tokenInfo = tokenInfos[key]; - require(tokenInfo.index > 0, "token not registered"); - require(tokenIndexer[tokenInfo.index] == key, "indexer exception"); - delete tokenInfos[key]; - delete tokenIndexer[tokenInfo.index]; +// File contracts/utils/TokenTransferHelper.sol +// License-Identifier: MIT + +library TokenTransferHelper { + function safeTransfer( + address token, + address receiver, + uint256 amount + ) internal { + (bool success, bytes memory data) = token.call(abi.encodeWithSelector( + IERC20.transfer.selector, + receiver, + amount + )); + require(success && (data.length == 0 || abi.decode(data, (bool))), "helix:transfer token failed"); } - function claimProtocolFeeIncome( - bytes32 _tokenInfoKey, - uint256 _amount, - address _receiver - ) onlyDao external { - TokenInfo memory tokenInfo = tokenInfos[_tokenInfoKey]; - require(tokenInfo.protocolFeeIncome > _amount, "not enough income"); - tokenInfos[_tokenInfoKey].protocolFeeIncome = tokenInfo.protocolFeeIncome - _amount; - - if (tokenInfo.sourceToken == address(0)) { - TokenTransferHelper.safeTransferNative(msg.sender, _amount); - } else { - TokenTransferHelper.safeTransfer(tokenInfo.sourceToken, _receiver, _amount); - } - emit FeeIncomeClaimed(_tokenInfoKey, _amount, _receiver); + function safeTransferFrom( + address token, + address sender, + address receiver, + uint256 amount + ) internal { + (bool success, bytes memory data) = token.call(abi.encodeWithSelector( + IERC20.transferFrom.selector, + sender, + receiver, + amount + )); + require(success && (data.length == 0 || abi.decode(data, (bool))), "helix:transferFrom token failed"); } - function registerLnProvider( - uint256 _remoteChainId, - address _sourceToken, - address _targetToken, - uint112 _baseFee, - uint16 _liquidityFeeRate, - uint112 _transferLimit - ) external { - bytes32 key = getTokenKey(_remoteChainId, _sourceToken, _targetToken); - TokenInfo memory tokenInfo = tokenInfos[key]; - require(tokenInfo.index > 0, "token not registered"); - bytes32 providerKey = getProviderKey(_remoteChainId, msg.sender, _sourceToken, _targetToken); + function safeTransferNative( + address receiver, + uint256 amount + ) internal { + (bool success,) = payable(receiver).call{value: amount}(""); + require(success, "helix:transfer native token failed"); + } +} - require(_liquidityFeeRate < LIQUIDITY_FEE_RATE_BASE, "liquidity fee too large"); - SourceProviderInfo memory providerInfo = srcProviders[providerKey]; - providerInfo.baseFee = _baseFee; - providerInfo.liquidityFeeRate = _liquidityFeeRate; - providerInfo.transferLimit = _transferLimit; +// File @zeppelin-solidity/contracts/utils/Context.sol@v4.7.3 +// License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (utils/Context.sol) - // we only update the field fee of the provider info - // if the provider has not been registered, then this line will register, otherwise update fee - srcProviders[providerKey] = providerInfo; - emit LnProviderUpdated(_remoteChainId, msg.sender, _sourceToken, _targetToken, _baseFee, _liquidityFeeRate, _transferLimit); +/** + * @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 depositPenaltyReserve( - address _sourceToken, - uint256 _amount - ) external payable { - bytes32 key = getProviderStateKey(_sourceToken, msg.sender); - uint256 updatedPanaltyReserve = penaltyReserves[key] + _amount; - penaltyReserves[key] = updatedPanaltyReserve; + function _msgData() internal view virtual returns (bytes calldata) { + return msg.data; + } +} - if (_sourceToken == address(0)) { - require(msg.value == _amount, "invalid penaltyReserve value"); - } else { - require(msg.value == 0, "value not need"); - TokenTransferHelper.safeTransferFrom( - _sourceToken, - msg.sender, - address(this), - _amount - ); - } - emit PenaltyReserveUpdated(msg.sender, _sourceToken, updatedPanaltyReserve); +// 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; } - function withdrawPenaltyReserve( - address _sourceToken, - uint256 _amount - ) external { - bytes32 key = getProviderStateKey(_sourceToken, msg.sender); - uint256 updatedPanaltyReserve = penaltyReserves[key] - _amount; - penaltyReserves[key] = updatedPanaltyReserve; + /** + * @dev Modifier to make a function callable only when the contract is not paused. + * + * Requirements: + * + * - The contract must not be paused. + */ + modifier whenNotPaused() { + _requireNotPaused(); + _; + } - if (_sourceToken == address(0)) { - TokenTransferHelper.safeTransferNative(msg.sender, _amount); - } else { - TokenTransferHelper.safeTransfer(_sourceToken, msg.sender, _amount); - } - emit PenaltyReserveUpdated(msg.sender, _sourceToken, updatedPanaltyReserve); + /** + * @dev Modifier to make a function callable only when the contract is paused. + * + * Requirements: + * + * - The contract must be paused. + */ + modifier whenPaused() { + _requirePaused(); + _; } - function providerPause( - uint256 _remoteChainId, - address _sourceToken, - address _targetToken - ) external { - bytes32 providerKey = getProviderKey(_remoteChainId, msg.sender, _sourceToken, _targetToken); - srcProviders[providerKey].pause = true; - emit LnProviderPaused(msg.sender, _remoteChainId, _sourceToken, _targetToken, true); + /** + * @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"); } - function providerUnpause( - uint256 _remoteChainId, - address _sourceToken, - address _targetToken - ) external { - bytes32 providerKey = getProviderKey(_remoteChainId, msg.sender, _sourceToken, _targetToken); - srcProviders[providerKey].pause = false; - emit LnProviderPaused(msg.sender, _remoteChainId, _sourceToken, _targetToken, false); + /** + * @dev Triggers stopped state. + * + * Requirements: + * + * - The contract must not be paused. + */ + function _pause() internal virtual whenNotPaused { + _paused = true; + emit Paused(_msgSender()); } - function totalFee( - uint256 _remoteChainId, - address _provider, - address _sourceToken, - address _targetToken, - uint112 _amount - ) external view returns(uint112) { - TokenInfo memory tokenInfo = getTokenInfo(_remoteChainId, _sourceToken, _targetToken); - SourceProviderInfo memory providerInfo = getProviderInfo(_remoteChainId, _provider, _sourceToken, _targetToken); - uint256 providerFee = uint256(providerInfo.baseFee) + uint256(providerInfo.liquidityFeeRate) * uint256(_amount) / LIQUIDITY_FEE_RATE_BASE; - require(providerFee < type(uint112).max, "overflow fee"); - return uint112(providerFee) + tokenInfo.config.protocolFee; + /** + * @dev Returns to normal state. + * + * Requirements: + * + * - The contract must be paused. + */ + function _unpause() internal virtual whenPaused { + _paused = false; + emit Unpaused(_msgSender()); } +} - function lockAndRemoteRelease(TransferParams calldata _params) whenNotPaused external payable { - // check transfer info - bytes32 tokenKey = getTokenKey(_params.remoteChainId, _params.sourceToken, _params.targetToken); - TokenInfo memory tokenInfo = tokenInfos[tokenKey]; - SourceProviderInfo memory providerInfo = getProviderInfo(_params.remoteChainId, _params.provider, _params.sourceToken, _params.targetToken); - require(providerInfo.transferLimit >= _params.amount && _params.amount > 0, "invalid transfer amount"); - uint256 providerFee = uint256(providerInfo.baseFee) + uint256(providerInfo.liquidityFeeRate) * uint256(_params.amount) / LIQUIDITY_FEE_RATE_BASE; - require(providerFee < type(uint112).max, "overflow fee"); - uint112 amountWithFeeAndPenalty = _params.amount + uint112(providerFee) + tokenInfo.config.penalty; - require(_params.totalFee >= providerFee + tokenInfo.config.protocolFee, "fee not matched"); - - // update provider state - bytes32 stateKey = getProviderStateKey(_params.sourceToken, _params.provider); - uint256 penaltyReserved = penaltyReserves[stateKey]; - require(penaltyReserved >= tokenInfo.config.penalty, "penalty reserve not enough"); - penaltyReserved -= tokenInfo.config.penalty; - penaltyReserves[stateKey] = penaltyReserved; - emit PenaltyReserveUpdated(_params.provider, _params.sourceToken, penaltyReserved); +// File contracts/ln/base/LnBridgeSourceV3.sol +// License-Identifier: MIT - // save lock info - uint256 remoteAmount = uint256(_params.amount) * 10**tokenInfo.config.targetDecimals / 10**tokenInfo.config.sourceDecimals; - require(remoteAmount < MAX_TRANSFER_AMOUNT, "overflow amount"); - bytes32 transferId = getTransferId(_params, uint112(remoteAmount)); - require(lockInfos[transferId].status == 0, "transferId exist"); - lockInfos[transferId] = LockInfo(amountWithFeeAndPenalty, uint64(block.timestamp), tokenInfo.index, LOCK_STATUS_LOCKED); - bytes32 idWithTimestamp = keccak256(abi.encodePacked(transferId, uint64(block.timestamp))); - emit TokenLocked(_params, transferId, idWithTimestamp, uint112(remoteAmount), uint112(providerFee), uint64(block.timestamp)); - // update protocol fee income - // leave the protocol fee into contract, and admin can withdraw this fee anytime - tokenInfos[tokenKey].protocolFeeIncome = tokenInfo.protocolFeeIncome + tokenInfo.config.protocolFee; - // transfer token - uint112 totalPayAmount = _params.amount + uint112(providerFee) + tokenInfo.config.protocolFee; - if (_params.sourceToken == address(0)) { - require(msg.value >= totalPayAmount, "value not enough"); - if (msg.value > totalPayAmount) { - // refund - TokenTransferHelper.safeTransferNative(msg.sender, msg.value - totalPayAmount); - } - } else { - require(msg.value == 0, "no value need"); - TokenTransferHelper.safeTransferFrom(_params.sourceToken, msg.sender, address(this), totalPayAmount); - } +/// @title LnBridgeSourceV3 +/// @notice LnBridgeSourceV3 is a contract to help user lock token and then trigger remote chain relay +/// @dev See https://github.com/helix-bridge/contracts/tree/master/helix-contract +contract LnBridgeSourceV3 is Pausable, AccessController { + uint256 constant public LOCK_TIME_DISTANCE = 15 minutes; + uint256 constant public SLASH_EXPIRE_TIME = 1 hours; + uint256 constant public MAX_TRANSFER_AMOUNT = type(uint112).max; + // liquidity fee base rate + // liquidityFee = liquidityFeeRate / LIQUIDITY_FEE_RATE_BASE * sendAmount + // totalProviderFee = baseFee + liquidityFee + uint256 constant public LIQUIDITY_FEE_RATE_BASE = 100000; + uint8 constant public LOCK_STATUS_LOCKED = 1; + uint8 constant public LOCK_STATUS_WITHDRAWN = 2; + uint8 constant public LOCK_STATUS_SLASHED = 3; + // the configure information can be updated + struct TokenConfigure { + // pay to system for each tx + uint112 protocolFee; + // Used to penalise relayer for each slashed transaction + uint112 penalty; + uint8 sourceDecimals; + uint8 targetDecimals; + } + // registered token info + struct TokenInfo { + TokenConfigure config; + // zero index is invalid + // use this index to indict the token info to save gas + uint32 index; + address sourceToken; + address targetToken; + // accumulated system revenues + uint256 protocolFeeIncome; + } + struct TransferParams { + uint256 remoteChainId; + address provider; + address sourceToken; + address targetToken; + uint112 totalFee; + uint112 amount; + address receiver; + // use this timestamp as the lock time + // can't be too far from the block that the transaction confirmed + // This timestamp can also be adjusted to produce different transferId + uint256 timestamp; + } + // hash(remoteChainId, sourceToken, targetToken) => TokenInfo + mapping(bytes32=>TokenInfo) public tokenInfos; + // the token index is used to be stored in lockInfo to save gas + mapping(uint32=>bytes32) public tokenIndexer; + // amountWithFeeAndPenalty = transferAmount + providerFee + penalty < type(uint112).max + // the status only has the following 4 values + // status == 0: lockInfo not exist -> can update to status 1 + // status == 1: lockInfo confirmed on source chain(has not been withdrawn or slashed) -> can update to status 2 or 3 + // status == 2: lockInfo has been withdrawn -> can't update anymore + // status == 3: lockInfo has been slashed -> can't update anymore + // we don't clean lockInfo after withdraw or slash to avoid the hash collision(generate the same transferId) + // when we wan't to get tokenInfo from lockInfo, we should get the key(bytes32) from tokenIndex, then get tokenInfo from key + struct LockInfo { + uint112 amountWithFeeAndPenalty; + uint32 tokenIndex; + uint8 status; } + // transferId => LockInfo + mapping(bytes32 => LockInfo) public lockInfos; - // we require the same token to withdrawn - function withdrawLiquidity( - bytes32[] calldata _transferIds, - uint256 _remoteChainId, - // provider is verified on the target chain - address _provider - ) external allowRemoteCall(_remoteChainId) { - require(_transferIds.length > 0, "invalid transferIds size"); - uint32 tokenIndex = lockInfos[_transferIds[0]].tokenIndex; - uint256 totalAmount = 0; - for (uint i = 0; i < _transferIds.length; i++) { - bytes32 transferId = _transferIds[i]; - LockInfo memory lockInfo = lockInfos[transferId]; - require(lockInfo.amountWithFeeAndPenalty > 0, "invalid transferId"); - require(lockInfo.tokenIndex == tokenIndex, "token index not matched"); - require(lockInfo.status == LOCK_STATUS_LOCKED, "token has been withdrawn"); + struct SourceProviderInfo { + uint112 baseFee; + uint16 liquidityFeeRate; + uint112 transferLimit; + bool pause; + } - totalAmount += lockInfo.amountWithFeeAndPenalty; - lockInfos[transferId].status = LOCK_STATUS_WITHDRAWN; - emit LiquidityWithdrawn(transferId, _provider, lockInfo.amountWithFeeAndPenalty); - } - bytes32 key = tokenIndexer[tokenIndex]; - TokenInfo memory tokenInfo = tokenInfos[key]; - require(tokenInfo.index == tokenIndex, "invalid token info"); + // hash(remoteChainId, provider, sourceToken, targetToken) => SourceProviderInfo + mapping(bytes32=>SourceProviderInfo) public srcProviders; + // for a special source token, all the path start from this chain use the same panaltyReserve + // 1. when a lock tx sent, the penaltyReserves decrease and the penalty move to lockInfo.amountWithFeeAndPenalty + // 2. when withdraw liquidity, it tries to move this penalty lockInfo.amountWithFeeAndPenalty back to penaltyReserves + // 3. when the penaltyReserves is not enough to support one lock tx, the provider is paused to work + // hash(sourceToken, provider) => penalty reserve + mapping(bytes32=>uint256) public penaltyReserves; - uint256 withdrawAmount = totalAmount; - // if penalty updated, the relayer may not redeposit - if (tokenInfo.config.penalty * _transferIds.length < withdrawAmount) { - // restore the penalty reserve - uint112 redepositPenalty = tokenInfo.config.penalty * uint112(_transferIds.length); - bytes32 stateKey = getProviderStateKey(tokenInfo.sourceToken, _provider); - uint256 penaltyReserved = penaltyReserves[stateKey] + uint256(redepositPenalty); - penaltyReserves[stateKey] = penaltyReserved; - withdrawAmount -= redepositPenalty; - emit PenaltyReserveUpdated(_provider, tokenInfo.sourceToken, penaltyReserved); - } + event TokenRegistered( + bytes32 key, + uint256 remoteChainId, + address sourceToken, + address targetToken, + uint112 protocolFee, + uint112 penalty, + uint32 index + ); + event TokenInfoUpdated(bytes32 tokenInfoKey, uint112 protocolFee, uint112 penalty, uint112 sourceDecimals, uint112 targetDecimals); + event FeeIncomeClaimed(bytes32 tokenInfoKey, uint256 amount, address receiver); + event TokenLocked( + TransferParams params, + bytes32 transferId, + uint112 targetAmount, + uint112 fee + ); + event LnProviderUpdated( + uint256 remoteChainId, + address provider, + address sourceToken, + address targetToken, + uint112 baseFee, + uint16 liquidityfeeRate, + uint112 transferLimit + ); + event PenaltyReserveUpdated(address provider, address sourceToken, uint256 updatedPanaltyReserve); + event LiquidityWithdrawn(bytes32[] transferIds, address provider, uint256 amount); + event TransferSlashed(bytes32 transferId, address provider, address slasher, uint112 slashAmount); + event LnProviderPaused(address provider, uint256 remoteChainId, address sourceToken, address targetToken, bool paused); - if (tokenInfo.sourceToken == address(0)) { - TokenTransferHelper.safeTransferNative(_provider, withdrawAmount); - } else { - TokenTransferHelper.safeTransfer(tokenInfo.sourceToken, _provider, withdrawAmount); - } + modifier allowRemoteCall(uint256 _remoteChainId) { + _verifyRemote(_remoteChainId); + _; } - function slash( - uint256 _remoteChainId, - bytes32 _transferId, - // slasher, amount and lnProvider is verified on the target chain - uint112 _sourceAmount, - address _lnProvider, - uint64 _timestamp, - address _slasher - ) external allowRemoteCall(_remoteChainId) { - LockInfo memory lockInfo = lockInfos[_transferId]; - require(lockInfo.status == LOCK_STATUS_LOCKED, "invalid lock status"); - require(lockInfo.amountWithFeeAndPenalty >= _sourceAmount, "invalid amount"); - bytes32 tokenKey = tokenIndexer[lockInfo.tokenIndex]; - TokenInfo memory tokenInfo = tokenInfos[tokenKey]; - lockInfos[_transferId].status = LOCK_STATUS_SLASHED; + function _verifyRemote(uint256 _remoteChainId) internal virtual {} - uint112 slashAmount = _sourceAmount; - // recheck the timestamp - // expired - if (_timestamp == lockInfo.timestamp && lockInfo.timestamp + SLASH_EXPIRE_TIME < block.timestamp) { - slashAmount = lockInfo.amountWithFeeAndPenalty; - // pause this provider if slashed - bytes32 providerKey = getProviderKey(_remoteChainId, _lnProvider, tokenInfo.sourceToken, tokenInfo.targetToken); - srcProviders[providerKey].pause = true; - emit LnProviderPaused(_lnProvider, _remoteChainId, tokenInfo.sourceToken, tokenInfo.targetToken, true); - } else { - // this means the slasher help the provider relay message and get no reward - // redeposit penalty(the fee and penalty) for lnProvider - bytes32 key = getProviderStateKey(tokenInfo.sourceToken, _lnProvider); - penaltyReserves[key] = penaltyReserves[key] + lockInfo.amountWithFeeAndPenalty - slashAmount; - } - // transfer slashAmount to slasher - if (tokenInfo.sourceToken == address(0)) { - TokenTransferHelper.safeTransferNative(_slasher, slashAmount); - } else { - TokenTransferHelper.safeTransfer(tokenInfo.sourceToken, _slasher, slashAmount); - } - emit TransferSlashed(_transferId, _lnProvider, _slasher, slashAmount); + function unpause() external onlyOperator { + _unpause(); } - function getProviderKey(uint256 _remoteChainId, address _provider, address _sourceToken, address _targetToken) pure public returns(bytes32) { - return keccak256(abi.encodePacked(_remoteChainId, _provider, _sourceToken, _targetToken)); + function pause() external onlyOperator { + _pause(); } - function getTokenKey(uint256 _remoteChainId, address _sourceToken, address _targetToken) pure public returns(bytes32) { - return keccak256(abi.encodePacked(_remoteChainId, _sourceToken, _targetToken)); + // register a new token pair by Helix Dao + // if the token pair has been registered, it will revert + // select an unused _index to save the tokenInfo, it's not required that the _index is continous or increased + function registerTokenInfo( + uint256 _remoteChainId, + address _sourceToken, + address _targetToken, + uint112 _protocolFee, + uint112 _penalty, + uint8 _sourceDecimals, + uint8 _targetDecimals, + uint32 _index + ) onlyDao external { + require(_index > 0, "invalid index"); + bytes32 key = getTokenKey(_remoteChainId, _sourceToken, _targetToken); + TokenInfo memory oldInfo = tokenInfos[key]; + require(oldInfo.index == 0, "token info exist"); + require(tokenIndexer[_index] == bytes32(0), "the index exist"); + TokenConfigure memory tokenConfig = TokenConfigure( + _protocolFee, + _penalty, + _sourceDecimals, + _targetDecimals + ); + tokenInfos[key] = TokenInfo( + tokenConfig, + _index, + _sourceToken, + _targetToken, + 0 + ); + tokenIndexer[_index] = key; + emit TokenRegistered(key, _remoteChainId, _sourceToken, _targetToken, _protocolFee, _penalty, _index); } - function getProviderStateKey(address _sourceToken, address provider) pure public returns(bytes32) { - return keccak256(abi.encodePacked(_sourceToken, provider)); + // update a registered token pair + // the key or index cannot be updated + // Attention! source decimals and target decimals + function updateTokenInfo( + uint256 _remoteChainId, + address _sourceToken, + address _targetToken, + uint112 _protocolFee, + uint112 _penalty, + uint8 _sourceDecimals, + uint8 _targetDecimals + ) onlyDao external { + bytes32 key = getTokenKey(_remoteChainId, _sourceToken, _targetToken); + TokenInfo memory tokenInfo = tokenInfos[key]; + require(tokenInfo.index > 0, "token not registered"); + tokenInfos[key].config = TokenConfigure( + _protocolFee, + _penalty, + _sourceDecimals, + _targetDecimals + ); + emit TokenInfoUpdated(key, _protocolFee, _penalty, _sourceDecimals, _targetDecimals); } - function getTransferId( - TransferParams memory _params, - uint112 _remoteAmount - ) public view returns(bytes32) { - return keccak256(abi.encodePacked( - block.chainid, - _params.remoteChainId, - _params.provider, - _params.sourceToken, - _params.targetToken, - _params.receiver, - _params.amount, - _remoteAmount, - _params.nonce - )); + // delete a token pair by Helix Dao + // This interface should be called with exceptional caution, only when correcting registration errors, to conserve index resources. + // Attention! DON'T delete a used token pair + function deleteTokenInfo(bytes32 key) onlyDao external { + TokenInfo memory tokenInfo = tokenInfos[key]; + require(tokenInfo.index > 0, "token not registered"); + require(tokenIndexer[tokenInfo.index] == key, "indexer exception"); + delete tokenInfos[key]; + delete tokenIndexer[tokenInfo.index]; } - function getTokenInfo(uint256 _remoteChainId, address _sourceToken, address _targetToken) view internal returns(TokenInfo memory) { - bytes32 key = keccak256(abi.encodePacked(_remoteChainId, _sourceToken, _targetToken)); - return tokenInfos[key]; + // claim the protocol fee + function claimProtocolFeeIncome( + bytes32 _tokenInfoKey, + uint256 _amount, + address _receiver + ) onlyDao external { + TokenInfo memory tokenInfo = tokenInfos[_tokenInfoKey]; + require(tokenInfo.protocolFeeIncome > _amount, "not enough income"); + tokenInfos[_tokenInfoKey].protocolFeeIncome = tokenInfo.protocolFeeIncome - _amount; + + if (tokenInfo.sourceToken == address(0)) { + TokenTransferHelper.safeTransferNative(_receiver, _amount); + } else { + TokenTransferHelper.safeTransfer(tokenInfo.sourceToken, _receiver, _amount); + } + emit FeeIncomeClaimed(_tokenInfoKey, _amount, _receiver); } - function getProviderInfo( + // called by lnProvider + // this func can be called to register a new or update an exist LnProvider info + function registerLnProvider( uint256 _remoteChainId, - address _provider, address _sourceToken, - address _targetToken - ) view internal returns(SourceProviderInfo memory) { - bytes32 key = keccak256(abi.encodePacked(_remoteChainId, _provider, _sourceToken, _targetToken)); - return srcProviders[key]; - } -} - -// File contracts/ln/interface/ILnBridgeSourceV3.sol -// License-Identifier: MIT - -interface ILnBridgeSourceV3 { - function slash( - uint256 _remoteChainId, - bytes32 _transferId, - uint112 _sourceAmount, - address _lnProvider, - uint64 _timestamp, - address _slasher - ) external; - function withdrawLiquidity( - bytes32[] calldata _transferIds, - uint256 _remoteChainId, - address _provider - ) external; -} - -// File contracts/ln/base/LnBridgeHelper.sol -// License-Identifier: MIT + address _targetToken, + uint112 _baseFee, + uint16 _liquidityFeeRate, + uint112 _transferLimit + ) external { + bytes32 key = getTokenKey(_remoteChainId, _sourceToken, _targetToken); + TokenInfo memory tokenInfo = tokenInfos[key]; + require(tokenInfo.index > 0, "token not registered"); + bytes32 providerKey = getProviderKey(_remoteChainId, msg.sender, _sourceToken, _targetToken); -library LnBridgeHelper { - // the time(seconds) for liquidity provider to delivery message - // if timeout, slasher can work. - uint256 constant public SLASH_EXPIRE_TIME = 60 * 60; - bytes32 constant public INIT_SLASH_TRANSFER_ID = bytes32(uint256(1)); - // liquidity fee base rate - // liquidityFee = liquidityFeeRate / LIQUIDITY_FEE_RATE_BASE * sendAmount - uint256 constant public LIQUIDITY_FEE_RATE_BASE = 100000; + require(_liquidityFeeRate < LIQUIDITY_FEE_RATE_BASE, "liquidity fee too large"); - struct TransferParameter { - bytes32 previousTransferId; - address provider; - address sourceToken; - address targetToken; - uint112 amount; - uint256 timestamp; - address receiver; - } + // we only update the field fee of the provider info + // if the provider has not been registered, then this line will register, otherwise update fee + SourceProviderInfo storage providerInfo = srcProviders[providerKey]; + providerInfo.baseFee = _baseFee; + providerInfo.liquidityFeeRate = _liquidityFeeRate; + providerInfo.transferLimit = _transferLimit; - // sourceToken and targetToken is the pair of erc20 token(or native) addresses - // if sourceToken == address(0), then it's native token - // if targetToken == address(0), then remote is native token - // * `protocolFee` is the protocol fee charged by system - // * `penaltyLnCollateral` is penalty from lnProvider when the transfer slashed, if we adjust this value, it'll not affect the old transfers. - struct TokenInfo { - uint112 protocolFee; - uint112 penaltyLnCollateral; - uint8 sourceDecimals; - uint8 targetDecimals; - bool isRegistered; + emit LnProviderUpdated(_remoteChainId, msg.sender, _sourceToken, _targetToken, _baseFee, _liquidityFeeRate, _transferLimit); } - function sourceAmountToTargetAmount( - TokenInfo memory tokenInfo, - uint112 amount - ) internal pure returns(uint112) { - uint256 targetAmount = uint256(amount) * 10**tokenInfo.targetDecimals / 10**tokenInfo.sourceDecimals; - require(targetAmount < type(uint112).max, "overflow amount"); - return uint112(targetAmount); - } + function depositPenaltyReserve( + address _sourceToken, + uint256 _amount + ) external payable { + bytes32 key = getProviderStateKey(_sourceToken, msg.sender); + uint256 updatedPanaltyReserve = penaltyReserves[key] + _amount; + penaltyReserves[key] = updatedPanaltyReserve; - function calculateProviderFee(uint112 baseFee, uint16 liquidityFeeRate, uint112 amount) internal pure returns(uint112) { - uint256 fee = uint256(baseFee) + uint256(liquidityFeeRate) * uint256(amount) / LIQUIDITY_FEE_RATE_BASE; - require(fee < type(uint112).max, "overflow fee"); - return uint112(fee); + if (_sourceToken == address(0)) { + require(msg.value == _amount, "invalid penaltyReserve value"); + } else { + require(msg.value == 0, "value not need"); + TokenTransferHelper.safeTransferFrom( + _sourceToken, + msg.sender, + address(this), + _amount + ); + } + emit PenaltyReserveUpdated(msg.sender, _sourceToken, updatedPanaltyReserve); } - function safeTransfer( - address token, - address receiver, - uint256 amount - ) internal { - (bool success, bytes memory data) = token.call(abi.encodeWithSelector( - IERC20.transfer.selector, - receiver, - amount - )); - require(success && (data.length == 0 || abi.decode(data, (bool))), "lnBridgeHelper:transfer token failed"); - } + function withdrawPenaltyReserve( + address _sourceToken, + uint256 _amount + ) external { + bytes32 key = getProviderStateKey(_sourceToken, msg.sender); + uint256 updatedPanaltyReserve = penaltyReserves[key] - _amount; + penaltyReserves[key] = updatedPanaltyReserve; - function safeTransferFrom( - address token, - address sender, - address receiver, - uint256 amount - ) internal { - (bool success, bytes memory data) = token.call(abi.encodeWithSelector( - IERC20.transferFrom.selector, - sender, - receiver, - amount - )); - require(success && (data.length == 0 || abi.decode(data, (bool))), "lnBridgeHelper:transferFrom token failed"); + if (_sourceToken == address(0)) { + TokenTransferHelper.safeTransferNative(msg.sender, _amount); + } else { + TokenTransferHelper.safeTransfer(_sourceToken, msg.sender, _amount); + } + emit PenaltyReserveUpdated(msg.sender, _sourceToken, updatedPanaltyReserve); } - function safeTransferNative( - address receiver, - uint256 amount - ) internal { - (bool success,) = payable(receiver).call{value: amount}(""); - require(success, "lnBridgeHelper:transfer native token failed"); + function providerPause( + uint256 _remoteChainId, + address _sourceToken, + address _targetToken + ) external { + bytes32 providerKey = getProviderKey(_remoteChainId, msg.sender, _sourceToken, _targetToken); + srcProviders[providerKey].pause = true; + emit LnProviderPaused(msg.sender, _remoteChainId, _sourceToken, _targetToken, true); } - function getProviderKey(uint256 remoteChainId, address provider, address sourceToken, address targetToken) pure internal returns(bytes32) { - return keccak256(abi.encodePacked( - remoteChainId, - provider, - sourceToken, - targetToken - )); + function providerUnpause( + uint256 _remoteChainId, + address _sourceToken, + address _targetToken + ) external { + bytes32 providerKey = getProviderKey(_remoteChainId, msg.sender, _sourceToken, _targetToken); + srcProviders[providerKey].pause = false; + emit LnProviderPaused(msg.sender, _remoteChainId, _sourceToken, _targetToken, false); } - function getTokenKey(uint256 remoteChainId, address sourceToken, address targetToken) pure internal returns(bytes32) { - return keccak256(abi.encodePacked( - remoteChainId, - sourceToken, - targetToken - )); + function totalFee( + uint256 _remoteChainId, + address _provider, + address _sourceToken, + address _targetToken, + uint112 _amount + ) external view returns(uint112) { + TokenInfo memory tokenInfo = getTokenInfo(_remoteChainId, _sourceToken, _targetToken); + SourceProviderInfo memory providerInfo = getProviderInfo(_remoteChainId, _provider, _sourceToken, _targetToken); + uint256 providerFee = uint256(providerInfo.baseFee) + uint256(providerInfo.liquidityFeeRate) * uint256(_amount) / LIQUIDITY_FEE_RATE_BASE; + require(providerFee < type(uint112).max, "overflow fee"); + return uint112(providerFee) + tokenInfo.config.protocolFee; } -} -// File contracts/ln/base/LnBridgeTargetV3.sol -// License-Identifier: MIT + function lockAndRemoteRelease(TransferParams calldata _params) whenNotPaused external payable { + // timestamp must be close to the block time + require( + _params.timestamp >= block.timestamp - LOCK_TIME_DISTANCE && _params.timestamp <= block.timestamp + LOCK_TIME_DISTANCE, + "timestamp is too far from block time" + ); + // check transfer info + bytes32 tokenKey = getTokenKey(_params.remoteChainId, _params.sourceToken, _params.targetToken); + TokenInfo memory tokenInfo = tokenInfos[tokenKey]; + SourceProviderInfo memory providerInfo = getProviderInfo(_params.remoteChainId, _params.provider, _params.sourceToken, _params.targetToken); + require(providerInfo.transferLimit >= _params.amount && _params.amount > 0, "invalid transfer amount"); + uint256 providerFee = uint256(providerInfo.baseFee) + uint256(providerInfo.liquidityFeeRate) * uint256(_params.amount) / LIQUIDITY_FEE_RATE_BASE; + require(providerFee < type(uint112).max, "overflow fee"); + uint112 amountWithFeeAndPenalty = _params.amount + uint112(providerFee) + tokenInfo.config.penalty; + require(_params.totalFee >= providerFee + tokenInfo.config.protocolFee, "fee not matched"); + require(!providerInfo.pause, "provider paused"); -contract LnBridgeTargetV3 { - // timestamp: the time when transfer filled, this is also the flag that the transfer is filled(relayed or slashed) - // provider: the transfer lnProvider - struct FillTransfer { - uint64 timestamp; - address provider; - } + // update provider state + bytes32 stateKey = getProviderStateKey(_params.sourceToken, _params.provider); + uint256 penaltyReserved = penaltyReserves[stateKey]; + require(penaltyReserved >= tokenInfo.config.penalty, "penalty reserve not enough"); + penaltyReserved -= tokenInfo.config.penalty; + penaltyReserves[stateKey] = penaltyReserved; + emit PenaltyReserveUpdated(_params.provider, _params.sourceToken, penaltyReserved); - // lockTimestamp: the time when the transfer start from source chain - // the lockTimestamp is verified on source chain, if slasher falsify this timestamp, then it can't be verified on source chain - // sourceAmount: the send amount on source chain - struct SlashInfo { - uint256 remoteChainId; - uint64 lockTimestamp; - uint112 sourceAmount; - address slasher; - } + // save lock info + uint256 remoteAmount = uint256(_params.amount) * 10**tokenInfo.config.targetDecimals / 10**tokenInfo.config.sourceDecimals; + require(remoteAmount < MAX_TRANSFER_AMOUNT && remoteAmount > 0, "overflow amount"); + bytes32 transferId = getTransferId(_params, uint112(remoteAmount)); + require(lockInfos[transferId].status == 0, "transferId exist"); + lockInfos[transferId] = LockInfo(amountWithFeeAndPenalty, tokenInfo.index, LOCK_STATUS_LOCKED); + emit TokenLocked(_params, transferId, uint112(remoteAmount), uint112(providerFee)); - struct RelayParams { - uint256 remoteChainId; - address provider; - address sourceToken; - address targetToken; - uint112 sourceAmount; - uint112 targetAmount; - address receiver; - uint256 nonce; - } + // update protocol fee income + // leave the protocol fee into contract, and admin can withdraw this fee anytime + tokenInfos[tokenKey].protocolFeeIncome = tokenInfo.protocolFeeIncome + tokenInfo.config.protocolFee; - // transferId => FillTransfer - mapping(bytes32 => FillTransfer) public fillTransfers; - // transferId => SlashInfo - mapping(bytes32 => SlashInfo) public slashInfos; + // transfer token + uint112 totalPayAmount = _params.amount + uint112(providerFee) + tokenInfo.config.protocolFee; + if (_params.sourceToken == address(0)) { + require(msg.value >= totalPayAmount, "value not enough"); + if (msg.value > totalPayAmount) { + // refund + TokenTransferHelper.safeTransferNative(msg.sender, msg.value - totalPayAmount); + } + } else { + require(msg.value == 0, "no value need"); + TokenTransferHelper.safeTransferFrom(_params.sourceToken, msg.sender, address(this), totalPayAmount); + } + } - event TransferFilled(bytes32 transferId, address provider); - event SlashRequest(bytes32 transferId, uint256 remoteChainId, address provider, address sourceToken, address targetToken, address slasher); + // we require the same token to withdrawn + function withdrawLiquidity( + bytes32[] calldata _transferIds, + uint256 _remoteChainId, + // provider is verified on the target chain + address _provider + ) external allowRemoteCall(_remoteChainId) { + require(_transferIds.length > 0, "invalid transferIds size"); + uint32 tokenIndex = lockInfos[_transferIds[0]].tokenIndex; + uint256 totalAmount = 0; + for (uint i = 0; i < _transferIds.length; i++) { + bytes32 transferId = _transferIds[i]; + LockInfo memory lockInfo = lockInfos[transferId]; + require(lockInfo.amountWithFeeAndPenalty > 0, "invalid transferId"); + require(lockInfo.tokenIndex == tokenIndex, "token index not matched"); + require(lockInfo.status == LOCK_STATUS_LOCKED, "token has been withdrawn"); - function _sendMessageToSource(uint256 _remoteChainId, bytes memory _payload, uint256 feePrepaid, bytes memory _extParams) internal virtual {} + totalAmount += lockInfo.amountWithFeeAndPenalty; + lockInfos[transferId].status = LOCK_STATUS_WITHDRAWN; + } + emit LiquidityWithdrawn(_transferIds, _provider, totalAmount); + bytes32 key = tokenIndexer[tokenIndex]; + TokenInfo memory tokenInfo = tokenInfos[key]; + require(tokenInfo.index == tokenIndex, "invalid token info"); - function relay( - RelayParams calldata _params, - bytes32 _expectedTransferId, - bool _relayBySelf - ) external payable { - // _relayBySelf = true to protect that the msg.sender don't relay for others - // _relayBySelf = false to allow that lnProvider can use different account between source chain and target chain - require(!_relayBySelf || _params.provider == msg.sender, "invalid provider"); - bytes32 transferId = keccak256(abi.encodePacked( - _params.remoteChainId, - block.chainid, - _params.provider, - _params.sourceToken, - _params.targetToken, - _params.receiver, - _params.sourceAmount, - _params.targetAmount, - _params.nonce - )); - require(_expectedTransferId == transferId, "check expected transferId failed"); - FillTransfer memory fillTransfer = fillTransfers[transferId]; - // Make sure this transfer was never filled before - require(fillTransfer.timestamp == 0, "transfer has been filled"); - fillTransfers[transferId] = FillTransfer(uint64(block.timestamp), _params.provider); + uint256 withdrawAmount = totalAmount; + // if penalty updated, the relayer may not redeposit + if (tokenInfo.config.penalty * _transferIds.length < withdrawAmount) { + // restore the penalty reserve + uint112 redepositPenalty = tokenInfo.config.penalty * uint112(_transferIds.length); + bytes32 stateKey = getProviderStateKey(tokenInfo.sourceToken, _provider); + uint256 penaltyReserved = penaltyReserves[stateKey] + uint256(redepositPenalty); + penaltyReserves[stateKey] = penaltyReserved; + withdrawAmount -= redepositPenalty; + emit PenaltyReserveUpdated(_provider, tokenInfo.sourceToken, penaltyReserved); + } - if (_params.targetToken == address(0)) { - require(msg.value == _params.targetAmount, "invalid amount"); - LnBridgeHelper.safeTransferNative(_params.receiver, _params.targetAmount); + if (tokenInfo.sourceToken == address(0)) { + TokenTransferHelper.safeTransferNative(_provider, withdrawAmount); } else { - LnBridgeHelper.safeTransferFrom(_params.targetToken, msg.sender, _params.receiver, uint256(_params.targetAmount)); + TokenTransferHelper.safeTransfer(tokenInfo.sourceToken, _provider, withdrawAmount); } - emit TransferFilled(transferId, _params.provider); } - function requestSlashAndRemoteRelease( - RelayParams calldata _params, - uint64 _timestamp, - bytes32 _expectedTransferId, - bytes32 _expectedIdWithTimestamp, - uint256 _feePrepaid, - bytes memory _extParams - ) external payable { - bytes32 transferId = keccak256(abi.encodePacked( - _params.remoteChainId, - block.chainid, - _params.provider, - _params.sourceToken, - _params.targetToken, - _params.receiver, - _params.sourceAmount, - _params.targetAmount, - _params.nonce - )); - require(_expectedTransferId == transferId, "check expected transferId failed"); - bytes32 idWithTimestamp = keccak256(abi.encodePacked(transferId, _timestamp)); - require(idWithTimestamp == _expectedIdWithTimestamp, "check timestamp failed"); - - FillTransfer memory fillTransfer = fillTransfers[transferId]; - require(fillTransfer.timestamp == 0, "transfer has been filled"); + function slash( + uint256 _remoteChainId, + bytes32 _transferId, + // slasher, amount and lnProvider is verified on the target chain + address _lnProvider, + address _slasher + ) external allowRemoteCall(_remoteChainId) { + LockInfo memory lockInfo = lockInfos[_transferId]; + require(lockInfo.status == LOCK_STATUS_LOCKED, "invalid lock status"); + bytes32 tokenKey = tokenIndexer[lockInfo.tokenIndex]; + TokenInfo memory tokenInfo = tokenInfos[tokenKey]; + lockInfos[_transferId].status = LOCK_STATUS_SLASHED; - require(_timestamp < block.timestamp - LnBridgeHelper.SLASH_EXPIRE_TIME, "time not expired"); - fillTransfers[transferId] = FillTransfer(uint64(block.timestamp), _params.provider); - slashInfos[transferId] = SlashInfo(_params.remoteChainId, _timestamp, _params.sourceAmount, msg.sender); + // pause this provider if slashed + bytes32 providerKey = getProviderKey(_remoteChainId, _lnProvider, tokenInfo.sourceToken, tokenInfo.targetToken); + srcProviders[providerKey].pause = true; + emit LnProviderPaused(_lnProvider, _remoteChainId, tokenInfo.sourceToken, tokenInfo.targetToken, true); - if (_params.targetToken == address(0)) { - require(msg.value == _params.targetAmount + _feePrepaid, "invalid value"); - LnBridgeHelper.safeTransferNative(_params.receiver, _params.targetAmount); + // transfer token to slasher + if (tokenInfo.sourceToken == address(0)) { + TokenTransferHelper.safeTransferNative(_slasher, lockInfo.amountWithFeeAndPenalty); } else { - require(msg.value == _feePrepaid, "value too large"); - LnBridgeHelper.safeTransferFrom(_params.targetToken, msg.sender, _params.receiver, uint256(_params.targetAmount)); + TokenTransferHelper.safeTransfer(tokenInfo.sourceToken, _slasher, lockInfo.amountWithFeeAndPenalty); } - bytes memory message = abi.encodeWithSelector( - ILnBridgeSourceV3.slash.selector, - block.chainid, - transferId, - _params.sourceAmount, - _params.provider, - _timestamp, - msg.sender - ); - _sendMessageToSource(_params.remoteChainId, message, _feePrepaid, _extParams); - emit SlashRequest(transferId, _params.remoteChainId, _params.provider, _params.sourceToken, _params.targetToken, msg.sender); + emit TransferSlashed(_transferId, _lnProvider, _slasher, lockInfo.amountWithFeeAndPenalty); } - function retrySlash(bytes32 transferId, bytes memory _extParams) external payable { - FillTransfer memory fillTransfer = fillTransfers[transferId]; - require(fillTransfer.timestamp > 0, "transfer not filled"); - SlashInfo memory slashInfo = slashInfos[transferId]; - require(slashInfo.slasher == msg.sender, "invalid slasher"); - // send message - bytes memory message = abi.encodeWithSelector( - ILnBridgeSourceV3.slash.selector, - block.chainid, - transferId, - slashInfo.sourceAmount, - fillTransfer.provider, - slashInfo.lockTimestamp, - slashInfo.slasher - ); - _sendMessageToSource(slashInfo.remoteChainId, message, msg.value, _extParams); + function getProviderKey(uint256 _remoteChainId, address _provider, address _sourceToken, address _targetToken) pure public returns(bytes32) { + return keccak256(abi.encodePacked(_remoteChainId, _provider, _sourceToken, _targetToken)); } - // can't withdraw for different providers each time - function requestWithdrawLiquidity( + function getTokenKey(uint256 _remoteChainId, address _sourceToken, address _targetToken) pure public returns(bytes32) { + return keccak256(abi.encodePacked(_remoteChainId, _sourceToken, _targetToken)); + } + + function getProviderStateKey(address _sourceToken, address provider) pure public returns(bytes32) { + return keccak256(abi.encodePacked(_sourceToken, provider)); + } + + function getTransferId( + TransferParams memory _params, + uint112 _remoteAmount + ) public view returns(bytes32) { + return keccak256(abi.encodePacked( + block.chainid, + _params.remoteChainId, + _params.provider, + _params.sourceToken, + _params.targetToken, + _params.receiver, + _params.amount, + _remoteAmount, + _params.timestamp + )); + } + + function getTokenInfo(uint256 _remoteChainId, address _sourceToken, address _targetToken) view internal returns(TokenInfo memory) { + bytes32 key = keccak256(abi.encodePacked(_remoteChainId, _sourceToken, _targetToken)); + return tokenInfos[key]; + } + + function getProviderInfo( uint256 _remoteChainId, - bytes32[] calldata _transferIds, address _provider, - bytes memory _extParams - ) external payable { - for (uint i = 0; i < _transferIds.length; i++) { - bytes32 transferId = _transferIds[i]; - FillTransfer memory fillTransfer = fillTransfers[transferId]; - require(fillTransfer.provider == _provider, "provider invalid"); - } - bytes memory message = abi.encodeWithSelector( - ILnBridgeSourceV3.withdrawLiquidity.selector, - _transferIds, - block.chainid, - _provider - ); - _sendMessageToSource(_remoteChainId, message, msg.value, _extParams); + address _sourceToken, + address _targetToken + ) view internal returns(SourceProviderInfo memory) { + bytes32 key = keccak256(abi.encodePacked(_remoteChainId, _provider, _sourceToken, _targetToken)); + return srcProviders[key]; } } diff --git a/helix-contract/test/5_test_ln_v3.js b/helix-contract/test/5_test_ln_v3.js index 4d69b443..089011bd 100644 --- a/helix-contract/test/5_test_ln_v3.js +++ b/helix-contract/test/5_test_ln_v3.js @@ -294,7 +294,7 @@ describe("lnv3 bridge tests", () => { // balance // srcChain: user -> source bridge contract [amount + providerFee + protocolFee] - async function transfer(direction, nonce, isNative) { + async function transfer(direction, timestamp, isNative) { const chainInfo = await getChainInfo(direction, isNative); const totalFee = Number(await chainInfo.srcBridge.totalFee( chainInfo.dstChainId, @@ -313,7 +313,7 @@ describe("lnv3 bridge tests", () => { totalFee, transferAmount, user.address, - nonce, + timestamp, ]; let value = 0; if (isNative) { @@ -344,7 +344,7 @@ describe("lnv3 bridge tests", () => { // balance // on target: relayer -> user - async function relay(direction, transferId, nonce, isNative) { + async function relay(direction, transferId, timestamp, isNative) { const chainInfo = await getChainInfo(direction, isNative); const balanceOfUser = await balanceOf(chainInfo.dstToken, user.address); const balanceOfRelayer = await balanceOf(chainInfo.dstToken, relayer.address); @@ -362,7 +362,7 @@ describe("lnv3 bridge tests", () => { transferAmount, targetAmount, user.address, - nonce, + timestamp, ], transferId, true, @@ -390,12 +390,8 @@ describe("lnv3 bridge tests", () => { console.log("relay gas used", relayGasUsed); } - async function slash(direction, nonce, expectedTransferId, timestamp, isNative) { + async function slash(direction, expectedTransferId, timestamp, isNative) { const chainInfo = await getChainInfo(direction, isNative); - let blockTimestamp = timestamp; - if (blockTimestamp === null) { - blockTimestamp = (await ethers.provider.getBlock("latest")).timestamp; - } const dstToken = await ethers.getContractAt("Erc20", chainInfo.dstToken); await dstToken.connect(slasher).approve(chainInfo.dstBridge.address, initTokenBalance); @@ -419,9 +415,8 @@ describe("lnv3 bridge tests", () => { transferAmount, targetAmount, user.address, - nonce, + timestamp, ], - blockTimestamp, expectedTransferId, feePrepaid, chainInfo.extParams, @@ -490,27 +485,28 @@ describe("lnv3 bridge tests", () => { await ethToken.connect(user).approve(ethBridge.address, initTokenBalance); // test normal transfer and relay // 1. transfer from eth to arb - const transferId01 = await transfer('eth2arb', 1, false); + let timestamp = (await ethers.provider.getBlock("latest")).timestamp; + const transferId01 = await transfer('eth2arb', timestamp, false); const blockTimestamp01 = (await ethers.provider.getBlock("latest")).timestamp; // 2. relay "transfer from eth to arb" - await relay('eth2arb', transferId01, 1, false); + await relay('eth2arb', transferId01, timestamp, false); // 3. repeat relay - await expect(relay('eth2arb', transferId01, 1, false)).to.be.revertedWith("transfer has been filled"); + await expect(relay('eth2arb', transferId01, timestamp, false)).to.be.revertedWith("transfer has been filled"); // test slash // 1. slash a relayed tx - await expect(slash("eth2arb", 1, transferId01, blockTimestamp01, false)).to.be.revertedWith("transfer has been filled"); + await expect(slash("eth2arb", transferId01, timestamp, false)).to.be.revertedWith("transfer has been filled"); // 2. slash a normal unrelayed tx - const transferId02 = await transfer('eth2arb', 2, false); + const transferId02 = await transfer('eth2arb', blockTimestamp01, false); const blockTimestamp02 = (await ethers.provider.getBlock("latest")).timestamp; // 2.1. slash when not expired - await expect(slash("eth2arb", 2, transferId02, blockTimestamp02, false)).to.be.revertedWith("time not expired"); + await expect(slash("eth2arb", transferId02, blockTimestamp01, false)).to.be.revertedWith("time not expired"); await hre.network.provider.request({ method: "evm_increaseTime", - params: [18001], + params: [3601], }); // 2.2. slashed - await slash("eth2arb", 2, transferId02, blockTimestamp02, false); + await slash("eth2arb", transferId02, blockTimestamp01, false); // withdraw await withdraw('eth2arb', [transferId01], true, false); @@ -518,44 +514,47 @@ describe("lnv3 bridge tests", () => { await withdraw('eth2arb', [transferId01], false, false); // withdraw a slashed transfer failed await withdraw('eth2arb', [transferId02], false, false); + console.log("eth2arb test finished"); } // test arb2eth direction { + let timestamp = (await ethers.provider.getBlock("latest")).timestamp; await arbToken.connect(user).approve(arbBridge.address, initTokenBalance); - const transferId11 = await transfer('arb2eth', 1, false); + const transferId11 = await transfer('arb2eth', timestamp, false); const blockTimestamp11 = (await ethers.provider.getBlock("latest")).timestamp; - await relay('arb2eth', transferId11, 1, false); - await expect(relay('arb2eth', transferId11, 1, false)).to.be.revertedWith("transfer has been filled"); + await relay('arb2eth', transferId11, timestamp, false); + await expect(relay('arb2eth', transferId11, timestamp, false)).to.be.revertedWith("transfer has been filled"); - await expect(slash("arb2eth", 1, transferId11, blockTimestamp11, false)).to.be.revertedWith("transfer has been filled"); - const transferId12 = await transfer('arb2eth', 2, false); + await expect(slash("arb2eth", transferId11, timestamp, false)).to.be.revertedWith("transfer has been filled"); + const transferId12 = await transfer('arb2eth', blockTimestamp11, false); const blockTimestamp12 = (await ethers.provider.getBlock("latest")).timestamp; - await expect(slash("arb2eth", 2, transferId12, blockTimestamp12, false)).to.be.revertedWith("time not expired"); + await expect(slash("arb2eth", transferId12, blockTimestamp11, false)).to.be.revertedWith("time not expired"); await hre.network.provider.request({ method: "evm_increaseTime", - params: [18001], + params: [3601], }); - await slash("arb2eth", 2, transferId12, blockTimestamp12, false); - console.log("test finished"); + await slash("arb2eth", transferId12, blockTimestamp11, false); + console.log("arb2eth test finished"); } // test native token { - const transferId21 = await transfer('arb2eth', 1, true); + let timestamp = (await ethers.provider.getBlock("latest")).timestamp; + const transferId21 = await transfer('arb2eth', timestamp, true); const blockTimestamp21 = (await ethers.provider.getBlock("latest")).timestamp; - await relay('arb2eth', transferId21, 1, true); - await expect(relay('arb2eth', transferId21, 1, true)).to.be.revertedWith("transfer has been filled"); + await relay('arb2eth', transferId21, timestamp, true); + await expect(relay('arb2eth', transferId21, timestamp, true)).to.be.revertedWith("transfer has been filled"); - await expect(slash("arb2eth", 1, transferId21, blockTimestamp21, true)).to.be.revertedWith("transfer has been filled"); - const transferId22 = await transfer('arb2eth', 2, true); + await expect(slash("arb2eth", transferId21, timestamp, true)).to.be.revertedWith("transfer has been filled"); + const transferId22 = await transfer('arb2eth', blockTimestamp21, true); const blockTimestamp22 = (await ethers.provider.getBlock("latest")).timestamp; - await expect(slash("arb2eth", 2, transferId22, blockTimestamp22, true)).to.be.revertedWith("time not expired"); + await expect(slash("arb2eth", transferId22, blockTimestamp21, true)).to.be.revertedWith("time not expired"); await hre.network.provider.request({ method: "evm_increaseTime", - params: [18001], + params: [3601], }); - await slash("arb2eth", 2, transferId22, blockTimestamp22, true); + await slash("arb2eth", transferId22, blockTimestamp21, true); console.log("test finished"); } });