Skip to main content

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 the ActorFactory) 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 or eth_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.

Connect Wallets
  1. User sends request to create Actor with the tx_datacalling 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 the CREATE2 opcode.)
  1. External calls the ActorFactory to deploy Actor with the tx_data calling Final SC

  2. ActorFactory deploys Actor using CREATE2 based on description of deploy function

  3. User asserts correct Actor address and sends the funds there (through the Bridge)

  4. Bridge wraps the funds and sends it to the Actor

  5. External listens to the Bridge events and once the funds are wrapped it calls execute

  6. Actor calls the FinalSC on behalf of User

  1. After execute is complete, Actor calls the withdraw which creates a request to the Bridge

  2. Bridge unwraps the funds back to the User

note

Subsequent interactions through Wrapped Smart Contracts will skip steps 2 and 3, since the Actor will alread be deployed.

note

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

ContractDescription
Actora 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.
ActorFactorya 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.
L1MsgVerifyis a precompiled contract that is used to verify the signature of the message signed on the Layer 1 mainchain.
CardanoSigVerificationis 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.