movementlabsxyz / movement

The Movement Network is a Move-based L2 on Ethereum.
Apache License 2.0
84 stars 68 forks source link

Implement Gas Fee and Estimation for full Bridge Transfer #781

Open 0xmovses opened 1 month ago

0xmovses commented 1 month ago

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

Primata commented 2 weeks 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.

0xmovses commented 2 weeks ago

I started it, on the relayer but now its different with Trusted Relayer. Go for it.

Primata commented 2 weeks ago

@apenzk @franck44 @0xmovses

There are a few approaches we could take here:

  1. 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
    }
  2. 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);
    }
  3. 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.

musitdev commented 2 weeks ago

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.

apenzk commented 2 weeks ago

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

franck44 commented 1 week ago

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.

apenzk commented 1 week ago

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.

  1. this approach has strong assumptions on the equality in number of transfers in both directions.
  2. IF equal amount of transfers.. we would need to overcharge by a factor 2 in L1->L2 direction
  3. IF equal amount of transfers.. we would need to overcharge by a factor 2 in L2->L1 direction
  4. since fee_transaction(L1) >> fee_transaction(L2), this would incentivise for only high-value transfers to go L1->L2, and a massive amount of transactions to go L2->L1.. consequently rapidly depleting the described L1 pool.

Hence I dont think this would work.

Primata commented 1 week ago

@apenzk @franck44 @0xmovses

There are a few approaches we could take here:

  1. 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
    }
  1. 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);
    }
  1. 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:

  1. 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.

  2. 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%.

  3. 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.