Direct transfer protocol
At a high level, our goal is to create a more seamless experience for a user to transact between protocols across chains. In this case, we look specifically at mechanisms to allow users to deposit into a protocol on the child chain while remaining on the parent chain.
The current common way of doing this would be to:
- bridge ether or ERC20 tokens over the native bridge
- trade / swap currencies using some dex or other form of exchange
- then finally, deposit into the protocol
We can see here that this will require multiple signatures with possibly multiple approvals as well that the user would be required to sign throughout this process. We would like to provide some infrastructure and tools to help dapps avoid this inconvenience by implementing a clone
which could hold funds on behalf of the user.
The key difference between this and a normal relayer is that this solution utilizes the native state synchronization updates that the bor
blockchain provides, eliminating the need to trust a relayer.
At a high level, we make some key observations about how polygon
's architecture and internals function.
- Polygon's PoS bridge is build on-top two major components. The first is Heimdall, the validator nodes that provide security via proof of stake, and the second is
Bor
, polygon's block producing layer. - Polygon can process many more transactions per unit of time than Ethereum can, so naturally, they will not keep pace with each other. As such, in order to keep state updates between the two chains, there will be periodic 'updates' to the entire state of the blockchain which may even represent the culmination of several transactions. Additionally, in typical scenarios where state synchronization needs to occur, usually a mapping is required from one chain to the other for a given pair of assets or state tunnel.
- In the event that you do not have an asset mapping, you can still send state updates cross chain by utilizing other already mapped contracts. In this case, the Fx Portal contracts are designed to do just that. To briefly summarize how this works:
- FxRoot is on Ethereum and is mapped to FxChild. These two can update state directly with each other
- Contract A is on Ethereum and would like to sync state updates with Contract B, for which there is no mapping from A to B.
- Contract A instead asks FxRoot to sync state updates with FxChild. Likewise, Contract B does the same and everyone is able to sync state without the need for a mapping really.
- The interesting thing to note here is that state sync does not actually just simply change the values held in storage. Instead,
Bor
actually fetches a batch of state updates from heimdall and _applies_them to the current world state by executing all of the state updates as if they were normal transactions. However, these 'transactions' are slightly different... For starters, they are executed as system calls, so no events will be emitted and they won't show up on etherscan like other transactions would. - Because these state sync updates happen in batches, there is anywhere between a 5-10 minute time delay for state updates to propagate through to polygon. However, Bor will always execute the updates, whenever they arrive in. Additionally, tokens and their token mappings, and in general - polygon's internals rely on these invariants being held true
Putting these observations together, we can piece together a high level view of what we are trying to accomplish:
Scenario: A user is on Ethereum, with currency A and wishes to deposit currency B into protocol P which is on Polygon.
- User can interact with our smart contract on Ethereum, sending funds or tokens there, instead of the native bridge.
- Our contract wraps up some additional metadata, and sends the tokens / assets over the bridge first, followed then by a state sync update for the metadata.
- On polygon, we can take advantage of how
bor
will automatically execute our defined functions as itsyncs state updates
. This is incredibly convenient as we avoid having to post a constant relayer to be watching our one contract for token arrivals. Since we sent the metadata over the bridge second (after the asset), then by the time we hit our execution environment, we can be sure that the assets / tokens have already arrived. - From there, we can further take advantage of the system call and have it do some heavy lifting for us (up to 1 million gas or so) - in particular,
- we use create2 to deploy a proxy contract which functionally acts as an account which can deposit or withdraw into a given protocol (minimal clones). Note that it's important that we use create2 here because when a user initiates one of these transactions, funds will actually bridge over before the deployment of a minimal clone. We rely on the difficult of reverse hashing here to ensure that an attacker could not somehow displace an intended clone deployment
- we swap tokens using some router and input data (could be any router / dex / dex aggregator). (recall funds should have already arrived if we are in the execution environment)
- deposit into a given protocol. Different protocols may have different rules or interests etc, so we provide a standard abstract contract for them to inherit and provide their own 'adapter' or 'implementation' for integrating with our core smart contracts. For example, we standardize the
User
object's deposit
function as follows:
interface IUser {
function deposit(bytes calldata depositData) external;
}
However, we still provide the flexibility of custom interactions / functionality by exposing these abstract functions and calling them internally:
abstract contract ProtocolUser {
__protocol_deposit(bytes calldata depositData) external;
}
contract User is ProtocolUser {
function deposit(bytes calldata depositData) external {
__protocol_deposit(depositData);
}
}