Overview
Wrapped smart contracts (WSC) are a powerful protocol that enables the execution of Layer 2 smart contracts directly from Layer 1 wallets. This integration is made possible through the provider object, which adheres to the EIP-1193 standard, acting as a bridge between the EVM provider and the Layer 1 provider. By using WSC, EVM DApps can seamlessly communicate with Layer 1 wallets and execute transactions on Layer 2 smart contracts.
Architecture
Wrapped Smart Contracts rely on three key components:
Smart Contracts - Similar to wallets like Flint or Metamask in Milkomeda C1, users typically have dedicated accounts. To simplify the process, a smart contract called
Actor
is utilized as an account abstraction, executing actions on behalf of the user.Oracle - The Oracle is a node.js JSON-RPC API that facilitates message relay between Layer 1 and Layer 2 actors. It provides essential methods for deploying the
Actor
(using theActorFactory
) and executing transactions on behalf of users.Provider - The front-end library injects the provider object based on EIP-1193 into the window.ethereum object. This customized provider includes specific methods like
eth_sendTransaction
oreth_requestAccounts
, which interact with the Layer 1 wallet and transform the result accordingly.
Protocol Flow
The full protocol flow is described below for a user interaction for the first time, where an Actor
proxy smart contract is deployed and linked the user's L1 address. Subsequent calls from the user will be made to that existing existing Actor
.
- User sends request to create Actor with the
tx_data
calling Final SC, and get the actor address back.tx_data
is an encoded transaction which the Actor will call on the Final SC. (The actor address is derived from the L1 address using theCREATE2
opcode.)
External calls the ActorFactory to deploy Actor with the
tx_data
calling Final SCActorFactory deploys Actor using CREATE2 based on description of deploy function
User asserts correct Actor address and sends the funds there (through the Bridge)
Bridge wraps the funds and sends it to the Actor
External listens to the Bridge events and once the funds are wrapped it calls execute
Actor calls the FinalSC on behalf of User
After execute is complete, Actor calls the withdraw which creates a request to the Bridge
Bridge unwraps the funds back to the User
Subsequent interactions through Wrapped Smart Contracts will skip steps 2 and 3, since the Actor
will alread be deployed.
The gas is being paid by the Actor smart contract itself, therefore to execute any transaction the actor needs to have enough balance not only to performa the trasaction but also to pay gas. To fund the Actor smart contract the User should use bridge or the DApp should prompt the wallet to send the funds to the Actor through the Bridge.
Smart contracts
Contract | Description |
---|---|
Actor | a smart contract to be deployed on the Milkomeda Layer 2 that is bound to the specific L1 address. It serves as an account abstraction: it has a balance, a nonce, and can execute signed transactions on behalf of the user. |
ActorFactory | a smart contract deployed on the Milkomeda Layer 2 that is used to deploy actors. It uses CREATE2 opcode to deterministically derive Actor address from the L1 address. |
L1MsgVerify | is a precompiled contract that is used to verify the signature of the message signed on the Layer 1 mainchain. |
CardanoSigVerification | is a precompiled contract that is deprecated, but needed for backwards compatibility with the milkomeda-c1 testnet. |
To verify the signature the actor will use the L1MsgVerify precompiled contract that can verify L1 signatures. The execution will be invoked by the oracle service, which will get exact refund for the gas paid during the execution. |
The ActorFactory has the deploy function the deploy the Actor
to an address using CREATE2. The address will be hash(0xFF, sender, salt, bytecode),
where sender is factory, so salt has to be, hash(user, tx_data, random) and random can be predictable so it can be block.timestamp.
function deploy(string calldata mainchainAddress, bytes32 salt) public returns (Actor) {
// Using CREATE2 to have deterministic actor address tied to the `mainchainAddress`
Actor actor = new Actor{salt: salt}(mainchainAddress, l1Type);
emit Deployed(address(actor), mainchainAddress, salt);
return actor;
}
Since the new address is the combination of bytecode
, user
and tx_data
, we can be sure that the deployed Actor
won't be corrupted, and we can also ssert the address on the clientside.
In the compile task the Actor
contract is compiled to yul code and prepended with the code to store the transaction gas limit to storage. The gas limit is crucial to the calculation of used gas and to verify that the oracle acted honestly and didn't provide less gas than user signed and therefore the transaction would run out of gas. The yul code is later compiled to the evm bytecode and edited in the artifacts.