diff --git a/helix-contract/contracts/ln/lnv2/base/LnBridgeHelper.sol b/helix-contract/contracts/ln/lnv2/base/LnBridgeHelper.sol index 535ac39..b2c9a12 100644 --- a/helix-contract/contracts/ln/lnv2/base/LnBridgeHelper.sol +++ b/helix-contract/contracts/ln/lnv2/base/LnBridgeHelper.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.17; import "@zeppelin-solidity/contracts/token/ERC20/IERC20.sol"; +import "@zeppelin-solidity/contracts/utils/cryptography/ECDSA.sol"; library LnBridgeHelper { // the time(seconds) for liquidity provider to delivery message @@ -102,5 +103,11 @@ library LnBridgeHelper { targetToken )); } + + function recoverSignature(bytes32 messageHash, bytes calldata signature) pure internal returns (address) { + bytes memory prefix = "\x19Ethereum Signed Message:\n32"; + bytes32 dataHash = keccak256(abi.encodePacked(prefix, messageHash)); + return ECDSA.recover(dataHash, signature); + } } diff --git a/helix-contract/contracts/ln/lnv2/base/LnOppositeBridgeSource.sol b/helix-contract/contracts/ln/lnv2/base/LnOppositeBridgeSource.sol index dcba7de..5d14dca 100644 --- a/helix-contract/contracts/ln/lnv2/base/LnOppositeBridgeSource.sol +++ b/helix-contract/contracts/ln/lnv2/base/LnOppositeBridgeSource.sol @@ -156,9 +156,17 @@ contract LnOppositeBridgeSource is Pausable { emit LnProviderUpdated(_remoteChainId, msg.sender, _sourceToken, _targetToken, config.margin, _baseFee, _liquidityFeeRate); } - // the fee user should paid when transfer. - // totalFee = providerFee + protocolFee - // providerFee = provider.baseFee + provider.liquidityFeeRate * amount + function dynamicTotalFee( + uint256 _remoteChainId, + address _provider, + address _sourceToken, + address _targetToken, + uint112 _amount, + uint112 _dynamicBaseFee + ) external view returns(uint256) { + return _totalFee(_remoteChainId, _provider, _sourceToken, _targetToken, _amount, _dynamicBaseFee); + } + function totalFee( uint256 _remoteChainId, address _provider, @@ -166,43 +174,71 @@ contract LnOppositeBridgeSource is Pausable { address _targetToken, uint112 _amount ) external view returns(uint256) { + return _totalFee(_remoteChainId, _provider, _sourceToken, _targetToken, _amount, 0); + } + + // the fee user should paid when transfer. + // totalFee = providerFee + protocolFee + // providerFee = provider.baseFee + provider.liquidityFeeRate * amount + function _totalFee( + uint256 _remoteChainId, + address _provider, + address _sourceToken, + address _targetToken, + uint112 _amount, + uint112 _dynamicBaseFee + ) internal view returns(uint256) { bytes32 providerKey = LnBridgeHelper.getProviderKey(_remoteChainId, _provider, _sourceToken, _targetToken); SourceProviderInfo memory providerInfo = srcProviders[providerKey]; - uint112 providerFee = LnBridgeHelper.calculateProviderFee(providerInfo.config.baseFee, providerInfo.config.liquidityFeeRate, _amount); + uint112 baseFee = providerInfo.config.baseFee; + if (_dynamicBaseFee > 0) { + baseFee = _dynamicBaseFee; + } + uint112 providerFee = LnBridgeHelper.calculateProviderFee(baseFee, providerInfo.config.liquidityFeeRate, _amount); bytes32 tokenKey = LnBridgeHelper.getTokenKey(_remoteChainId, _sourceToken, _targetToken); return providerFee + tokenInfos[tokenKey].protocolFee; } + function transferAndLockMargin( + Snapshot calldata _snapshot, + uint112 _amount, + address _receiver + ) external payable { + _transferAndLockMargin(_snapshot, _receiver, _amount, 0); + } + + function transferAndLockMarginWithDynamicFee( + Snapshot calldata _snapshot, + uint112 _amount, + address _receiver, + uint112 _baseFee, + uint64 _expire, + bytes calldata _signature + ) external payable { + require(_expire >= block.timestamp, "the signature expired"); + bytes32 messageHash = keccak256(abi.encodePacked(_baseFee, _expire)); + address provider = LnBridgeHelper.recoverSignature(messageHash, _signature); + require(provider == _snapshot.provider, "invalid signer"); + _transferAndLockMargin(_snapshot, _receiver, _amount, _baseFee); + } + // This function transfers tokens from the user to LnProvider and generates a proof on the source chain. // The snapshot represents the state of the LN bridge for this LnProvider, obtained by the off-chain indexer. // If the chain state is updated and does not match the snapshot state, the transaction will be reverted. // 1. the state(lastTransferId, fee, margin) must match snapshot // 2. transferId not exist - function transferAndLockMargin( + function _transferAndLockMargin( Snapshot calldata _snapshot, + address _receiver, uint112 _amount, - address _receiver - ) whenNotPaused external payable { + uint112 _dynamicBaseFee + ) whenNotPaused internal { require(_amount > 0, "invalid amount"); - bytes32 providerKey = LnBridgeHelper.getProviderKey(_snapshot.remoteChainId, _snapshot.provider, _snapshot.sourceToken, _snapshot.targetToken); - SourceProviderInfo memory providerInfo = srcProviders[providerKey]; - - require(!providerInfo.config.pause, "provider paused"); - LnBridgeHelper.TokenInfo memory tokenInfo = tokenInfos[ LnBridgeHelper.getTokenKey(_snapshot.remoteChainId, _snapshot.sourceToken, _snapshot.targetToken) ]; - uint112 providerFee = LnBridgeHelper.calculateProviderFee(providerInfo.config.baseFee, providerInfo.config.liquidityFeeRate, _amount); - - // the chain state not match snapshot - require(providerInfo.lastTransferId == _snapshot.transferId, "snapshot expired"); - // Note: this requirement is not enough to ensure that the lnProvider's margin is enough because there maybe some frozen margins in other transfers - require(providerInfo.config.margin >= _amount + tokenInfo.penaltyLnCollateral + providerFee, "amount not valid"); - require(_snapshot.depositedMargin <= providerInfo.config.margin, "margin updated"); - require(_snapshot.totalFee >= tokenInfo.protocolFee + providerFee, "fee is invalid"); - uint112 targetAmount = LnBridgeHelper.sourceAmountToTargetAmount(tokenInfo, _amount); require(targetAmount > 0, "invalid amount"); require(block.timestamp < type(uint32).max, "timestamp overflow"); @@ -215,12 +251,28 @@ contract LnOppositeBridgeSource is Pausable { _snapshot.targetToken, _receiver, targetAmount)); + + uint112 providerFee; + { + bytes32 providerKey = LnBridgeHelper.getProviderKey(_snapshot.remoteChainId, _snapshot.provider, _snapshot.sourceToken, _snapshot.targetToken); + SourceProviderInfo memory providerInfo = srcProviders[providerKey]; + require(!providerInfo.config.pause, "provider paused"); + + uint112 baseFee = _dynamicBaseFee > 0 ? _dynamicBaseFee : providerInfo.config.baseFee; + providerFee = LnBridgeHelper.calculateProviderFee(baseFee, providerInfo.config.liquidityFeeRate, _amount); + // the chain state not match snapshot + require(providerInfo.lastTransferId == _snapshot.transferId, "snapshot expired"); + // Note: this requirement is not enough to ensure that the lnProvider's margin is enough because there maybe some frozen margins in other transfers + require(providerInfo.config.margin >= _amount + tokenInfo.penaltyLnCollateral + providerFee, "amount not valid"); + require(_snapshot.depositedMargin <= providerInfo.config.margin, "margin updated"); + require(_snapshot.totalFee >= tokenInfo.protocolFee + providerFee, "fee is invalid"); + // update the state to prevent other transfers using the same snapshot + srcProviders[providerKey].lastTransferId = transferId; + } + require(lockInfos[transferId].timestamp == 0, "transferId exist"); lockInfos[transferId] = LockInfo(_amount, tokenInfo.penaltyLnCollateral + providerFee, uint32(block.timestamp), false); - // update the state to prevent other transfers using the same snapshot - srcProviders[providerKey].lastTransferId = transferId; - if (_snapshot.sourceToken == address(0)) { require(_amount + _snapshot.totalFee == msg.value, "amount unmatched"); LnBridgeHelper.safeTransferNative(_snapshot.provider, _amount + providerFee); diff --git a/helix-contract/test/3_test_ln_opposite.js b/helix-contract/test/3_test_ln_opposite.js index 32c97e6..9714f21 100644 --- a/helix-contract/test/3_test_ln_opposite.js +++ b/helix-contract/test/3_test_ln_opposite.js @@ -4,6 +4,7 @@ const chai = require("chai"); const ethUtil = require('ethereumjs-util'); const abi = require('ethereumjs-abi'); const secp256k1 = require('secp256k1'); +const { config } = require("hardhat"); chai.use(solidity); @@ -207,6 +208,22 @@ describe("eth->arb lnv2 positive bridge tests", () => { liquidityFeeRate ); + async function signFee(privateKey, fee, expire) { + const key = ethers.utils.arrayify(privateKey); + const messageHash = ethers.utils.solidityKeccak256(['uint112', 'uint64'], [fee, expire]); + const wallet = new ethers.Wallet(privateKey); + // can also use this method to sign message + //const signature = await wallet.signMessage(ethers.utils.arrayify(messageHash)); + + const dataHash = ethers.utils.solidityKeccak256(['bytes', 'bytes'], [ethers.utils.toUtf8Bytes('\x19Ethereum Signed Message:\n32'), messageHash]); + const signatureECDSA = secp256k1.ecdsaSign(ethers.utils.arrayify(dataHash), key); + const ethRecID = signatureECDSA.recid + 27; + const signature = Uint8Array.from( + signatureECDSA.signature.join().split(',').concat(ethRecID) + ); + return ethers.utils.hexlify(signature); + } + async function getChainInfo(direction) { if (direction === 'eth2arb') { return { @@ -560,5 +577,50 @@ describe("eth->arb lnv2 positive bridge tests", () => { // !warning there is a bug for lnv2 to slash a native token cross transfer with opposite bridge } + + // test signed baseFee + { + const dynamicBaseFee = 1234; + const expire = await getBlockTimestamp() + 100; + const totalFee = Number(await arbBridge.dynamicTotalFee( + ethChainId, + relayer.address, + arbToken.address, + ethToken.address, + transferAmount, + dynamicBaseFee + )); + const providerKey = getProviderKey(ethChainId, relayer.address, arbToken.address, ethToken.address); + const providerInfo = await arbBridge.srcProviders(providerKey); + const lastTransferId = providerInfo.lastTransferId; + const leftMargin = providerInfo.config.margin; + const relayerPrivateKey = config.networks.hardhat.accounts[1].privateKey; + const signature = await signFee(relayerPrivateKey, dynamicBaseFee, expire); + const balanceOfUser = await arbToken.balanceOf(user.address); + const balanceOfRelayer = await arbToken.balanceOf(relayer.address); + const tx = await arbBridge.connect(user).transferAndLockMarginWithDynamicFee( + [ + ethChainId, + relayer.address, + arbToken.address, + ethToken.address, + lastTransferId, + totalFee, + leftMargin + ], + transferAmount, + user.address, + dynamicBaseFee, + expire, + signature + ); + const recipient = await tx.wait(); + gasUsed = recipient.cumulativeGasUsed; + console.log("transferAndLockMarginWithDynamicFee gas used", gasUsed); + const balanceOfUserAfter = await arbToken.balanceOf(user.address); + const balanceOfRelayerAfter = await arbToken.balanceOf(relayer.address); + expect(balanceOfUser - balanceOfUserAfter).to.equal(totalFee + transferAmount); + expect(balanceOfRelayerAfter - balanceOfRelayer).to.equal(transferAmount + totalFee - protocolFee); + } }); });