-
Notifications
You must be signed in to change notification settings - Fork 124
[protocol 3.6] Bridge and Converter #2218
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
Conversation
packages/loopring_v3/contracts/core/impl/libexchange/ExchangeDeposits.sol
Outdated
Show resolved
Hide resolved
packages/loopring_v3/contracts/aux/access/LoopringIOExchangeOwner.sol
Outdated
Show resolved
Hide resolved
packages/loopring_v3/contracts/aux/access/LoopringIOExchangeOwner.sol
Outdated
Show resolved
Hide resolved
packages/loopring_v3/contracts/aux/access/LoopringIOExchangeOwner.sol
Outdated
Show resolved
Hide resolved
packages/loopring_v3/contracts/aux/access/LoopringIOExchangeOwner.sol
Outdated
Show resolved
Hide resolved
packages/loopring_v3/contracts/aux/access/LoopringIOExchangeOwner.sol
Outdated
Show resolved
Hide resolved
packages/loopring_v3/contracts/aux/access/LoopringIOExchangeOwner.sol
Outdated
Show resolved
Hide resolved
# Conflicts: # packages/loopring_v3/contracts/aux/access/LoopringIOExchangeOwner.sol # packages/loopring_v3/test/testDebugTools.ts # packages/loopring_v3/test/testExchangeUtil.ts
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only partially done. The rest will take a few more days:(
packages/loopring_v3/contracts/core/impl/libexchange/ExchangeDeposits.sol
Show resolved
Hide resolved
packages/loopring_v3/contracts/core/impl/libexchange/ExchangeDeposits.sol
Show resolved
Hide resolved
packages/loopring_v3/contracts/core/impl/libexchange/ExchangeBlocks.sol
Outdated
Show resolved
Hide resolved
packages/loopring_v3/contracts/core/impl/libexchange/ExchangeBlocks.sol
Outdated
Show resolved
Hide resolved
packages/loopring_v3/contracts/aux/access/LoopringIOExchangeOwner.sol
Outdated
Show resolved
Hide resolved
packages/loopring_v3/contracts/aux/access/LoopringIOExchangeOwner.sol
Outdated
Show resolved
Hide resolved
packages/loopring_v3/contracts/core/impl/libtransactions/WithdrawTransaction.sol
Outdated
Show resolved
Hide resolved
| // Verify the transactions | ||
| for (uint g = 0; g < connectorCall.groups.length; g++) { | ||
| ConnectorGroup memory group = connectorCall.groups[g]; | ||
| for (uint i = 0; i < group.calls.length; i++) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm having a hard time understanding the notion of groups. If we use adding liquidity of two tokens into Uniswap as an example, in such a group, there are two transfers and one Join. So maybe the group should have multiple transfers with one call. But the current implementation seems to assume each call has one matched transfer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A group is per specific operation. So for a tokenA -> tokenB swap, a group would simply be a list of tokenA transfers with.
For batch migration a group could also just be a list of all kinds of tokens.
For adding liquidity for tokenA/tokenB the group would just contain 2 bridge calls from the same owner doing the 2 transfers. But you're right, maybe it does indeed make sense to more tightly encode these together for cases like these with multiple token transfers (similar to how we do things for AMM joints/exits). It could potentially make cases with multiple transfers a little bit cheaper, but I think only worth if it doesn't make the more normal cases more expensive.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, lets talk about this later after your optimizations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Kept the bridge call with 1 transfer/bridge call for now to keep things a bit simpler but also the gas cost increased quite a bit the way I implemented it with more arrays that needed to passed on-chain (which mostly contained a single element, but still has some noticeable overhead of ~20% gas cost increase because we're only talking about ~13,000 gas per bridge call, so even ~2,000 gas extra is quite a lot). Most common scenario's will very likely do just a single transfer so I think it should be fine, and multiple bridge calls to do multiple transfers can still easily be submitted together.
|
The bridge enabled two functionalities: 1) batch deposits which involves one deposit and multiple L2 distribution, and 2) batch withdrawal that invloves mutliple L2 collections and one withdrawal and potential L2 dapp interaction. I wonder if we can better abstract these two functionalities into their easy-to-understand names, something like |
* [Bridge+AMM] Optimizations + fixes * [Bridge] Misc improvements * Add migration test connector * Misc small improvements * Misc fixes + tests
|
Maybe we should bump version to 3.7 |
Not that many changes to the core protocol except the flash minting (pretty minor chance). But some version bump seems necessary, 3.7 seems logical. |
# Conflicts: # packages/loopring_v3/contracts/core/impl/ExchangeV3.sol
https://github.com/Loopring/protocols/wiki/L2-Vaults
Initial idea for using flash loans here: #1196 (comment)
Bridge
The Bridge works similarly to an AMM pool in that the contract is the owner of an account on layer 2, and on-chain we verify and approve transactions from and to this account. This is the bridge that is used to communicate from and to Loopring. Most times the Bridge account and contract contains no funds, it's simply used as an intermediate. To do communication with Loopring the bridge allows doing just two things.
1. Batch deposits
There is a
batchDepositcall that can be done by any external contract that simply deposits a number of tokens to the Bridge account on L2. The functions also takes in a list of transfers that need to be done on layer 2 from the bridge account to all other accounts in this specified list. So this way only one normal deposit for each distinct token is done, and all other batched deposits are done by simple transfers from the bridge account to the actual destination address on L2. The specified transfers are hashed and stored on-chain so we can later process them in a block, an event is logged with the data to make fetching this list of transfers easy. A uniquebatchIDis assigned to each batched deposit to make sure the hash is unique and can't fail. The operator doesn't have to do all these batch deposits, if they are too old they can be withdrawn from the bridge contract directly as a safety for both the operator (spam) and users (operator stops working). The bridge also allows a small max fee to be used from the batched deposits to pay for the L2 transfers.2. Bridge calls
The bridge also allows calling external contracts efficiently (with funds) using batching with the help of
Connectors. The idea of theConnectoris that an integration with some dApp is wanted, but we need to be able to do these efficiently in a batched way for all kinds of different scenarios like:All these can work differently for batching so we leave that up to the connector to implement, and so the bridge just calls the standard function
processCallsto let the connector do what's necessary. In some cases these connectors just need to do another L1 call and that's it. In other cases, for example a token swap, another token is bought that needs to be returned to the buyer on L2. This is where the batched deposits come in again to allow the connector to do just that in a standard way.These connectors also need to handle the funds in some way. In all cases all funds are first moved to the Bridge contract. This was chosen as a way to not impose any risks on all funds in the deposit contract, I think it's too risky to even call trusted functions directly on the deposit contract. But the current method is pretty efficient, in many cases there is only a need to do as many L1 token transfers as there are unique tokens to and from the Bridge contract, even if multiple connectors are called.
All connectors can do everything, but there is the concept of trusted and untrusted connectors for efficiency reasons, which gas a small implementation difference:
processCallson trusted connectors using a delegate call. This means no additional token transfers are needed to execute the code in the connector, but the connector has access to all funds currently being processed in the bridge.processCallson non-trusted connectors using a real call after transferring the necessary funds to the connector contract. This makes sure that only funds of users that want to interact with that connector are at risk in that connector.What is a trusted and untrusted connector can be set by the Bridge owner. Implementation wise this is only a very small difference at the Bridge side (the connector doesn't need to know), but for gas efficiency it can be worth it to make the connector trusted after we make sure it's safe. On the other hand, untrusted connectors are also great because users can call any connector without us having to worry about the connector misbehaving and putting other people's funds at risk.
No matter if the connector is trusted or not, connectors are given a max gas limit and are always allowed to fail in the
processCallscall. If the call fails the Bridge automatically approves the transfers back from the Bridge to the user's account.There is also the notion of a connector group. This is simply a way that the same connector can be used with all kinds of inputs. For example, for a uniswap connector a group would be the two tokens that need to be swapped, and this data is encoded in
groupDataand grouped together so all these swaps can be handled together.userDataon the other hand is for user specific data a connector could us. Think something like max slippage for a swap, or the destination address for a mass migration to a different rollup.Calls submitted to the connector will be sorted on group and tokens so they can be easily processed.
Similarly to the AMM pools, the user can sign a
BridgeCalltransaction which is verified on-chain using theSignatureVerificationtx so the L2 keys can be used. So every bridge call requires 2 L2 transactions: an L2 transfer and the signature verification tx.Fees
When people sign a transaction they don't know how big a batch it will be part of. The operator itself also doesn't always know how much a bridge call will end up costing.
To solve this problem users sign a transaction and a pay fee for an abosolute amount of gas that the operator needs to provide to the batched bridge calls. The total amount of gas the operator needs to provide to the connector is at least the sum of all these gas amounts.
The other problem is that, when just using this mechanism, the operator could submit a single bridge call that is very likely to have low gas amount just by itself, which will fail because the whole operation will take much more gas than what that single bridge call provides. And so there is an additional
IBridgeConnector.getMinGasLimitwhich also forces the operator to at least provide the minimum amount of gas required for all the calls by the connector itself.Converters
Converters are the implementation of the scheme described here. They are very efficient at doing many conversions from tokenA -> tokenB, but each converter needs its own token and contract deployed.BaseConverterhandles all the general code that can be reused between all converters (failure logic for the conversion, but also the main vault stuff like the token code, burning, minting, depositing, withdrawing,...). Specific implementations just need to implement a simpleconvertfunction to do the specific implementation for the conversion. There is no problem with rounding errors for the vault token transfers if the user doesn't pay a fee on this token (which I think wouldn't make a lot of sense) because the vault token is transferred in and out using a float representation.Everything Converters can do, the Bridge can do as well, but a bit less efficient in some cases (although the Bridge could also be more efficient in others). On the other, the Bridge can do everything converters can do and more (like mass migrations between L2s).
Example integrations
Converter
The converter can do anything that can be represented by a token <-> token trade, under parameters shared by everyone in the batch. Each conversion from tokenA -> tokenB needs a separate contract.
Bridge
The bridge can do anything the Converter can do and more. For example, the bridge can do anything that can be represented by a token <-> token trade, but now with parameters that can be defined by each user separately. The bridge also supports operations that only require tokens to go out (like mass migration). Only a single
BridgeConnectorcontract needs to be deployed to enable a certain kind of L1 operation for all tokens. For example, just a single Uniswap connector is able to convert between any tokens.Even L1 operations that are not batchable could have some small benefits by using the bridge because the token withdrawals/deposits from/to the exchange can be shared by all bridge operations.
Flash minting