Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support dynamic fee for opposite lnv2 #72

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions helix-contract/contracts/ln/lnv2/base/LnBridgeHelper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
}

100 changes: 76 additions & 24 deletions helix-contract/contracts/ln/lnv2/base/LnOppositeBridgeSource.sol
Original file line number Diff line number Diff line change
Expand Up @@ -156,53 +156,89 @@ 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,
address _sourceToken,
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");
Expand All @@ -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);
Expand Down
62 changes: 62 additions & 0 deletions helix-contract/test/3_test_ln_opposite.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}
});
});
Loading