Open 0xmovses opened 1 month ago
@0xmovses have you worked on this? I'd be happy to have a take on it. https://github.com/movementlabsxyz/MIP/pull/58/files#r1850256760 here is my idea on how to implement it.
I started it, on the relayer but now its different with Trusted Relayer. Go for it.
@apenzk @franck44 @0xmovses
There are a few approaches we could take here:
We charge a fixed fee in move on outgoing transactions set by the relayer https://github.com/movementlabsxyz/aptos-core/pull/101
/// Charge bridge fee to the initiate bridge transfer.
///
/// @param initiator The signer representing the initiator.
/// @param amount The amount to be charged.
/// @return The new amount after deducting the bridge fee.
public(friend) fun charge_bridge_fee(initiator: &signer, amount: u64
) acquires BridgeConfig : u64 {
let bridge_config = borrow_global<BridgeConfig>(@aptos_framework);
let bridge_fee = bridge_config.bridge_fee;
let new_amount = amount - bridge_fee;
assert!(new_amount > 0, EINVALID_VALUE);
let bridge_fee_receiver = borrow_global<BridgeConfig>(@aptos_framework).bridge_operator;
coin::transfer_from_sender_to_receiver(initiator_address, bridge_fee_receiver, bridge_fee);
new_amount
}
We charge a fee estimated by the relayer as a parameter https://github.com/movementlabsxyz/movement/pull/887
function _completeBridgeTransfer(
bytes32 bridgeTransferId,
bytes32 initiator,
address recipient,
uint256 amount,
uint256 nonce,
uint256 bridgeFee
) internal {
// _l2l1RateLimit(amount);
// Ensure the bridge transfer ID is valid against the initiator, recipient, amount, and nonce
require(
bridgeTransferId == keccak256(abi.encodePacked(initiator, recipient, amount, nonce)),
InvalidBridgeTransferId()
);
// Ensure the bridge transfer has not already been completed
require(noncesToIncomingBridgeTransferIds[nonce] == bytes32(0x0), CompletedBridgeTransferId());
// Store the nonce to bridge transfer ID
noncesToIncomingBridgeTransferIds[nonce] = bridgeTransferId;
uint256 newAmount = _chargeBridgeFee(amount);
// Transfer the MOVE tokens to the recipient
if (!moveToken.transfer(recipient, newAmount)) revert MOVETransferFailed();
emit BridgeTransferCompleted(bridgeTransferId, initiator, recipient, amount, nonce);
}
We have one of these solutions then we eventually switch to getting the sqrtX96 price and sending that amount to the an operator https://etherscan.io/address/0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8#readContract#F11. It should be substantially expensive.
It's mostly for Eth->Mvt transfer because fee issue is mostly on Eth. My opinion: Solution 2: The relayer define the free in the Tx, it can calculate them from the initiate_tx done on Eth and adjust the cost in move (it needs some conversion mechanism that should be monitored to avoid wrong value). This solution is better for movement because we have a better estimation of the fee, but the user doesn't know them before starting the transfer. Solution 1: the user can know the cost before the transfer, but mvt can lose a lot in fee. Solution 2 another way: the UI calculate an estimation of the cost of the transfer and add it to the initiate_transfer call. The relayer get this estimation and compare with its Tx execution estimation and move conversion. If they are near, it takes the UI one. Otherwise, it uses its estimation. I think the main difficulty when estimating is you need to be near as much as possible to the estimation proposed to the use most of the time. Doing a transfer without knowing the fee will stop a lot of user. Changing the estimated fee for a transfer in an important manner will make the user very unhappy. My opinion is: propose a fee to the user. If the executed Eth tx change a lot, the relayer cancels the transfer and ask the user to do it another time when fee are lower. Propose in the UI a possibility to force the transfer at any fee.
It's mostly for Eth->Mvt transfer because fee issue is mostly on Eth.
the costly and thus problematic case for the fee estimate is $L2MOVE to $L1MOVE (L2->L1). Where the user needs to cover L1 fee costs in $L2MOVE
So could you clarify what exactly we want to do here?
From L1 to L2, the relayer pays fees on the L2 Txs: do we sponsor them or charge them (in a way that needs to be defined)? From L2 to L1, the relayer pays fees in Eth (L1). Same question, do we sponsor or charge?
If we charge, what is the main idea to do it (that's more tokenomics than technical I suppose)? Do we use oracles?
On simple way to do it is to charge (in Eth) the user on L1 a bit more than the Tx fees (for initiate) and set aside in a pool on L1. Use this pool for the relayer to relay L2 -> L1 transfers.
And on L2, do the same: over-charge the user (initiate), set the fees aside in an L2 pool, and use the pool for the relayer to pay fro L1->L2 relay txs.
Charging users would happen in the bridge UI.
On simple way to do it is to charge (in Eth) the user on L1 a bit more than the Tx fees (for initiate) and set aside in a pool on L1. Use this pool for the relayer to relay L2 -> L1 transfers. And on L2, do the same: over-charge the user (initiate), set the fees aside in an L2 pool, and use the pool for the relayer to pay fro L1->L2 relay txs.
Hence I dont think this would work.
@apenzk @franck44 @0xmovses
There are a few approaches we could take here:
- We charge a fixed fee in move on outgoing transactions set by the relayer Add Bridge Fee to Move Bridge Module aptos-core#101
/// Charge bridge fee to the initiate bridge transfer. /// /// @param initiator The signer representing the initiator. /// @param amount The amount to be charged. /// @return The new amount after deducting the bridge fee. public(friend) fun charge_bridge_fee(initiator: &signer, amount: u64 ) acquires BridgeConfig : u64 { let bridge_config = borrow_global<BridgeConfig>(@aptos_framework); let bridge_fee = bridge_config.bridge_fee; let new_amount = amount - bridge_fee; assert!(new_amount > 0, EINVALID_VALUE); let bridge_fee_receiver = borrow_global<BridgeConfig>(@aptos_framework).bridge_operator; coin::transfer_from_sender_to_receiver(initiator_address, bridge_fee_receiver, bridge_fee); new_amount }
- We charge a fee estimated by the relayer as a parameter Add ETH Bridge Fee specified by Relayer #887
function _completeBridgeTransfer( bytes32 bridgeTransferId, bytes32 initiator, address recipient, uint256 amount, uint256 nonce, uint256 bridgeFee ) internal { // _l2l1RateLimit(amount); // Ensure the bridge transfer ID is valid against the initiator, recipient, amount, and nonce require( bridgeTransferId == keccak256(abi.encodePacked(initiator, recipient, amount, nonce)), InvalidBridgeTransferId() ); // Ensure the bridge transfer has not already been completed require(noncesToIncomingBridgeTransferIds[nonce] == bytes32(0x0), CompletedBridgeTransferId()); // Store the nonce to bridge transfer ID noncesToIncomingBridgeTransferIds[nonce] = bridgeTransferId; uint256 newAmount = _chargeBridgeFee(amount); // Transfer the MOVE tokens to the recipient if (!moveToken.transfer(recipient, newAmount)) revert MOVETransferFailed(); emit BridgeTransferCompleted(bridgeTransferId, initiator, recipient, amount, nonce); }
- We have one of these solutions then we eventually switch to getting the sqrtX96 price and sending that amount to the an operator https://etherscan.io/address/0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8#readContract#F11. It should be substantially expensive.
@franck44 there are three proposals here:
We set a fee on L2 and that's how much we charge them for performing the transaction on the L1. On the backend, we estimate the current fee and add a percentage (I would advocate for +20%) from the current ethereum/move exchange value. If the price raises, we set it to current value +20%. We only drop the price if it falls 20% bellow the current gas price. At the start we can put an exorbitant price (300 move) if we don't have an exchange value to prohibit bridging back as that was always the intention. The user is informed of the amount they intend to bridge and we add the fee on top.
The relayer pushes a fee price as an argument to feePrice on L1 or L2 (code is in solidity, but I would advocate for that to happen on L2) and that is reduced from the amount the user pushes through the bridge. The user is informed of the amount to be reduced. We also charge the current gas price + 20%.
We wait for an ETH-MOVE LP to be deployed and use that as reference to pull the price. We do not perform the exchange, just check if the amount is enough or not. We warn the user on L2 that they will be charged and recommend a minimum amount and estimate the current exchange price. Tokens are transferred to a funds manager. I don't think this is a good idea.
All bridge operations must be net positive (if only life could be like this, fortunately, in computer programming it can).
We need add an additional
setFee
method on the Initiator contracts. This fee will charge the necessary amount + some for gas slippage that will cover the gas cost take on by the relayer for the full bridge transfer.Before the initiate call a full estimation across both chains must occur and send an argument to the contract in order to set the current fee, the user must then pay this fee along with the amount of MOVE they want to transfer.
See the MD authored by @0xPrimata that offers some ideas for implementing this. https://github.com/movementlabsxyz/MIP/pull/17