gnolang / hackerspace

Tinker, build, explore Gno - without the monorepo!
7 stars 4 forks source link

Flippando Builder Journey #33

Open michelleellen opened 10 months ago

michelleellen commented 10 months ago

Hey Dragos,

Welcome to the Gno.land Grant Program, I've started an issue for you to share updates on Flippando and introduce yourself to the team and community.

irreverentsimplicity commented 9 months ago

Flippando is a fully on-chain memory game, aiming at creating a virtual economy of goods. Initial implementation was in Solidity. This space documents porting the game to Gno.

Game mechanics

The user is presented with a square matrix, which must be "uncovered". Each matrix is made of pairs of different visual shapes and colors. In this initial version, we have 3 different tile types: colors, dice and hexagrams. In future versions we will use more basic geometric shapes (circle, square, triangle, hexagon), letters, logos (in sponsored games), etc.

User taps two distinct, uncovered yet squares. A request is sent to the blockchain, which alters the state of the smart contract. The client app, a React/next.js frontend, receives the event after the state is modified, which may have 2 outcomes:

In both cases, the state is persisted on chain.

As the game advances, when there are only sqrt(boardSize) tiles uncovered (e.g.: boardSize is 16, so we have a 4x4 matrix, when we have only 4 tiles uncovered) the logic changes. This is marked in the UI by displaying a string like: "Flippando is heating, entering unstable quantum state". From now on, each request will return a matching pair of tiles, so the last two requests are always solving the game. The caveat here is that, if any of these squares has been uncovered before, its color might change when the tile is "solved".

After the entire matrix is solved, it is turned into an on-chain NFT (Base64 encoding it into the abi, using SVG). Along with the NFT minting, an ERC 20 token, called "Flip", is also minted, and it's associated with the NFT, by being "locked" into it. The ERC20 token cannot be spent unless it's unlocked. We will see later on how it can be unlocked. This NFT/ERC20 token pair is what we call a "primitive" for the rest of the game functionality.

Levels

The game starts with a 4x4 matrix. After uncovering 8 such matrixes, the game advances to the next level, which uses 8x8 matrixes (64 tiles).

Goal

The goal of the game is to generate NFT primitives, or basic shapes that can be then assembled on-chain as art. Each NFT primitive can also be traded in a marketplace, so people looking for a specific combination needed for their digital painting can buy it, if it was already generated by someone else. Or they can choose to play the game again and again, until randomness will generate their specific tile.

A buying event will break the level logic. For instance, if someone buys one of your 8 level 1 matrixes, you will be left with only 7, so yo need to generate a new one before being able to play at next level.

The "painting" is done by dragging and dropping squares into an empty canvas.

Assembling NFT primitives in larger, more complex creations, unlocks the ERC20 token. In other words, every time one of the NFT primitives, created by solving a matrix, is used in a complex NFT, in a "painting", its "locked" ERC20 token is unlocked and sent to the creator of the basic NFT. There's a caveat: you cannot use your own basic NFTs to create art, you must use other people assets, unlocking the tokens for them.

The resulting, "art" NFT, can be sold in a normal NFT marketplace. At the moment of the writing of this document, the actual NFT display is using a proprietary approach, but we're aiming at implementing EIP 6150/GRC 6150. A normal marketplace will not see the combined NFTs, but a placeholder. This is by design. As we stabilize our composite NFT structure, we may use other ways.

Randomness

Although the game proposition is that we have to uncover a matrix, the matrix starts empty. There is no pre-generated matrix that the user can inspect by looking up the smart contract variables. Instead, we generate random numbers on each request (in the matrix interval: so, if we have a 4x4 matrix, we only use 4 shapes, so we generate random numbers in the 1 - 4 interval).

This approach reduces the possibility of cheating and validates each generated NFT as PoA (Proof of Attention).

Status

At the moment of writing, the base logic (random number generation and game state) has been implemented in a Gno package. Currently working on the realm that will handle user logic.

About

I'm a mobile app developer for 10+ years, with an interest in the technical part of crypto for 7 years. I'm familiar with maintaining PoS nodes. Flippando started as my pet project for learning Solidity. To my surprise, it won 2 hackathons this year, the Glitch hackathon in Incheon, South Korea, and the online Saga hackathon.

waymobetta commented 9 months ago

@irreverentsimplicity This is awesome to hear about. How are you managing the randomness with both Gno as well as with the EVM chains- any oracles being used (ie, VRF)?

irreverentsimplicity commented 9 months ago

@waymobetta Thank you, happy to hear you're interested. In Solidity, I am doing a very simple block-based random generation, which, obviously, in some rollups gives very weird results - some tiles are coming up mono-color, because there's no real random number generated. In Gno I didn't get there yet, still organizing the app logic in packages / realms - once this is done, I will move on to actually implementing true randomness.

While obviously important, I don't see randomness as a make-it-or-break-it component for this game, at least not for now. The goal is to generate tiles which then are assembled in higher hierarchy "pieces of art", like bottom to top puzzles, by other people. True randomness is important to make sure the tiles are PoA (proof of attention) and not tampered with by manipulating the random number to obtain a specific tile design.

But let's assume the game gets popular. Someone who wants to "cheat" will have to:

This is like MEV-level effort, and if we ever get there, I think we'll be Ethereum-level popular - which is not a bad place to be. We will find ways to cross the randomness bridge, once we get there.

Of course, my assumptions can be wrong, and I'm always open to criticism and brainstorming.

irreverentsimplicity commented 9 months ago

Later edit: I assumed that you're referring to true random generation, for very basic testing I'm using this code:

func generateRandomNumbers(num, start, end int) []int {
    if start > end {
        start, end = end, start // Ensure the range is in ascending order
    }

    if num < 0 {
        ufmt.Sprintf("The number of random values (num) must be non-negative")
        return nil
    }

    if num > end-start+1 {
        ufmt.Sprintf("The number of random values (num) cannot exceed the range size")
        return nil
    }

    // Seed the random number generator with the current time
    rand.Seed(time.Now().UnixNano())
    // Generate num unique random values within the range
    result := make([]int, 0, num)
    for len(result) < num {
        randomValue := rand.Intn(end-start+1) + start

        // Check if the value already exists in the result slice
        // If not, append it to the result slice
        unique := true
        for _, val := range result {
            if val == randomValue {
                unique = false
                break
            }
        }

        if unique {
            result = append(result, randomValue)
        }
    }

    return result
}
waymobetta commented 9 months ago

@irreverentsimplicity I see, thanks for the explanation. I was thinking true randomness which is not simple, but you may be able to eventually incorporate VRF realm functionality from Teritori to satisfy your randomness needs; you can also read more within the use-case section.

irreverentsimplicity commented 9 months ago

@waymobetta thanks! I'm familiar with Teritori, the project, but not with their work on VRF, this looks like something that fits very well in my use case, I'll definitely keep an eye on it!

irreverentsimplicity commented 9 months ago

October 3rd 2023 Updates

TLDR: during the last month I've been making progress in the following areas:

Onboarding

Backend

Frontend

Contract versioning brainstorming


Onboarding

The installing and building process are very straightforward, I had no difficulties here. What took a little more time was familiarizing myself with the folder structure, specifically examples, demo and p / r folders. Given we are so early, though, this is expected and not at all a major issue.

I found the existence of packages and realms to be shaping the app development processes in a different way. In Solidity, for instance, we have a monolithic functionality package (even if spread over multiple contracts), whereas in Gno we may start organizing the app into basic, agnostic logic (packages), which then can be called by different realms. Realms, in turn, can be accessed via specific UIs. It's almost like a 3-tier system, which seem to offer significantly more flexibility.

Backend

One of the most important issues I faced was a CORS-related problem which made local dev unusable. The issue was fixed swiftly, though. For reference, here's the PR: https://github.com/gnolang/gno/pull/1118, thanks to @zivkovicmilos.

Although I do most of the development using the recommended TDD pattern, there are still use cases in which I need to redeploy a package. To allow that, I had to "brute force" my local dev env in the following way:

I made a small shell script, which I paste here in case it is useful (I reckon we will be soon transitioning to a versioning system, see below, in brainstorming).

cd testdir/data
rm -rf blockstore.db gnolang.db state.db cs.wal
cp priv_validator_state.json.orig priv_validator_state.json
cd ../../
gnoland start

(should be run in gno/gno.land folder). It's hacky and ugly, but for now it does the job.

I also started to split the logic into a package, flippandoserver, and a realm, flippando. flippandoserver will encapsulate an agnostic logic for generating pairs of random numbers, and updating a board, keeping the state of a game. flippando will encapsulate the logic for user games, basic NFT creation (solved boards) and composite NFT creation (assembling basic NFTs into higher hierarchy NFTs, "art").

Since the composite NFTs are arrays of basic NFTs, I need a new structure for it, so I'm looking at porting ERC6150 https://eips.ethereum.org/EIPS/eip-6150 (which has a tree-like structure) into a GRC6155 interface. There is already some code in my repo for it, once I'm happy with it, I'll open a PR. I'm also pondering adding ERC6220 https://eips.ethereum.org/EIPS/eip-6220.

Frontend

I was able to install and use both gno-js-client and the Adena wallet. I find both tools mature and stable, very easy to work with. I am using gno-js-client for testing realms and calling functions in read-only mode, and Adena to broadcast transactions to the chain. Still unsure if there is an event-like pattern to get the return values of a transaction, like in Solidity, or I have to use a different approach.

Contract versioning brainstorming

note: if there is more interest for this, I'm happy to put the text below in a separate issue

In Solidity, there is a proxy-based pattern that allows a contract to be upgraded. This is achieved by deploying a proxy in front of the actual contract, which maintains in its state the address(es) of the actual contract(s). Calls are made to the proxy, which then forwards them to the contracts. When a contract is upgraded, the proxy state is modified with the new address.

I tried to do something like this in gno/go, and I used SetImplementation, here's some basic code:

flippando.gno

package flippando

type GameInterface interface {
    StartGame() string
    EndGame() string
}

var currentImplementation GameInterface

func SetImplementation(impl GameInterface) {
    // todo: make it only owner
    currentImplementation = impl
}

func StartGame() string {
    if currentImplementation == nil {
        return "No implementation set"
    }
    return currentImplementation.StartGame()
}

func EndGame() string {
    if currentImplementation == nil {
        return "No implementation set"
    }
    return currentImplementation.EndGame()
}

flippandoV1.gno

// flippando/v1.go
package flippando

type FlippandoV1 struct{}

func (v *FlippandoV1) StartGame() string {
    return "V1: Game Started"
}

func (v *FlippandoV1) EndGame() string {
    return "V1: Game Ended"
}

flippandoV2.gno

// flippando/v2.go
package flippando

type FlippandoV2 struct{}

func (v *FlippandoV2) StartGame() string {
    return "V2: Game Started"
}

func (v *FlippandoV2) EndGame() string {
    return "V2: Game Ended"
}

// Only in V2
func (v *FlippandoV2) GetUserGames() string {
    return "V2: Fetching user games..."
}

flippando_test.gno

package flippando

import (
    "math/rand"
    "sort"
    "std"
    "testing"
    "time"

    "gno.land/p/demo/testutils"
)

func TestFlippandoV1(t *testing.T) {
    // Set the current implementation to V1
    SetImplementation(&FlippandoV1{})

    // Test StartGame for V1
    if result := StartGame(); result != "V1: Game Started" {
        t.Errorf("Expected 'V1: Game Started', got '%s'", result)
    }

    // Test EndGame for V1
    if result := EndGame(); result != "V1: Game Ended" {
        t.Errorf("Expected 'V1: Game Ended', got '%s'", result)
    }
}

func TestFlippandoV2(t *testing.T) {
    // Set the current implementation to V2
    SetImplementation(&FlippandoV2{})

    // Test StartGame for V2
    if result := StartGame(); result != "V2: Game Started" {
        t.Errorf("Expected 'V2: Game Started', got '%s'", result)
    }

    // Test EndGame for V2
    if result := EndGame(); result != "V2: Game Ended" {
        t.Errorf("Expected 'V2: Game Ended', got '%s'", result)
    }

    // Test GetUserGames for V2
    v2 := &FlippandoV2{}
    if result := v2.GetUserGames(); result != "V2: Fetching user games..." {
        t.Errorf("Expected 'V2: Fetching user games...', got '%s'", result)
    }
}

This is just a basic example, and it still doesn't allow for arbitrary package names. I started to look into keeping a registry and use reflect:

package uflippando

import (
    "reflect"
    "errors"
)

type GameInterface interface {
    StartGame() string
    EndGame() string
}

var registry map[string]GameInterface = make(map[string]GameInterface)

func RegisterImplementation(key string, pkg reflect.Value) error {
    // Iterate over the package's exported functions
    for i := 0; i < pkg.NumMethod(); i++ {
        method := pkg.Method(i)

        // Check if the method matches our naming scheme or struct tag
        if isValidGameImplementation(method.Name) {
            registry[key] = method.Interface().(GameInterface)
            return nil
        }
    }

    return errors.New("Valid game implementation not found")
}

func isValidGameImplementation(name string) bool {
    return name == "GameImplementationName" 
}

At this point I stopped and used my brute-force approach to local dev env, as it just allows me to move faster with the actual game implementation. As I said, if there's more interest about this, I can put it in an issue.

Bottom line

If I can maintain the current rhythm, meaning if nothing major happens, I think we are on track for having a functional gno flippando by the end of October.

irreverentsimplicity commented 7 months ago

The gno implementation of Flippando had its first live demo at the public Dev Call on November 8th. The demo included:

Challenges:

The Joy Of Randomness

While taking on the task of porting math/rand, I realized that in gno we're currently missing the standard Log function in the math package. This was the only blocker in implementing basic randomness, with just one call in the NormalFloat64 distribution. I decided to create a custom implementation of the Log function, using some hard coded values and I replaced the call of Log with it. This is a hack, and it is not intended to be used in a public math.rand port. It is good enough for the level of randomness we need in the game at this moment.

Ideally, the rand call should be replaced once there is a stable port of the crypto/rand go library.

The custom rand call in the math_random package should also be part of the flippandoserver package, to avoid any confusion with the actual math/rand package.

SetTokenURI

The gno implementation of the ERC721 interface, grc721, doesn't have a public method for setting the tokenURI (which exists, in the form of an avl.Tree). There's an open PR here to add this to the current trunk. Until then, for the purpose of the demo, I created a custom package, grc721f, which is referred as such throught the codebase. Once the PR is merged, I'll update the call with the public grc721 implementation.

Hacking dev env

Most of the development followed the standard TDD practices, but in the final stages I needed to actually upload all the packages to the chain. There were 2 custom basic modules: math_random (with a _) and grc721f (as opposed to the live grc721) and the game modules: a package flippandoserver and a realm, flippando. So, each time I made a significant modification, I had this workflow:

  1. reset the chain (starting from genesis) (reset.sh)
  2. upload all modules, in order (flippandoDeply.sh)
  3. fund the accounts needed in the game (sendTokens.sh)

I am pasting the actual shell scripts here, maybe someone will find them useful:

reset.sh

#/bin/bash

cd testdir/data
rm -rf blockstore.db gnolang.db state.db cs.wal
cp priv_validator_state.json.orig priv_validator_state.json
cd ../../
gnoland start

^^^^ the above needs a copy of the priv_validator_state.json, called priv_validator_state.json.orig. After the first successful launch of the chain, just do a cp priv_validator_state.json priv_validator_state.json.orig and keep that file around. The above script need to be run from gno/gno.land

flippandoDeploy.sh

#/bin/bash

echo "Deploying math_rand to math_rand..."

gnokey maketx addpkg  \
-deposit="1ugnot" \
-gas-fee="1ugnot" \
-gas-wanted="6000000" \
-broadcast="true" \
-remote="localhost:26657" \
-chainid="dev" \
-pkgdir="gno.land/p/demo/math_rand" \
-pkgpath="gno.land/p/demo/math_rand" \
test

echo "Deploying grc721f to grc721f..."

gnokey maketx addpkg  \
-deposit="1ugnot" \
-gas-fee="1ugnot" \
-gas-wanted="6000000" \
-broadcast="true" \
-remote="localhost:26657" \
-chainid="dev" \
-pkgdir="gno.land/p/demo/grc/grc721f" \
-pkgpath="gno.land/p/demo/grc/grc721f" \
test

echo "Deploying flippandoserver..."

gnokey maketx addpkg  \
-deposit="1ugnot" \
-gas-fee="1ugnot" \
-gas-wanted="6000000" \
-broadcast="true" \
-remote="localhost:26657" \
-chainid="dev" \
-pkgdir="gno.land/p/demo/flippandoserver" \
-pkgpath="gno.land/p/demo/flippandoserver" \
test

echo "Deploying flippando..."

gnokey maketx addpkg  \
-deposit="1ugnot" \
-gas-fee="1ugnot" \
-gas-wanted="8000000" \
-broadcast="true" \
-remote="localhost:26657" \
-chainid="dev" \
-pkgdir="gno.land/r/demo/flippando" \
-pkgpath="gno.land/r/demo/flippando" \
test

sendTokens.sh

#!/bin/bash
gnokey maketx send \
-gas-fee="10ugnot" \
-gas-wanted="5000000" \
-broadcast="true" \
-remote="localhost:26657" \
-chainid="dev" \
-to="the_address_that_needs_funds" \
-send="1000000000ugnot" test

the above 2 scripts need to be run from gno/examples.

Current status

You can now follow the development status by tracking the public board here.

thehowl commented 7 months ago

Hey @irreverentsimplicity, reading a bit of backlog here now :)

In Solidity, there is a proxy-based pattern that allows a contract to be upgraded

We will have something slightly different; the idea I discussed with Jae was to make the realm upgrade mechanism an "implicit proxy pattern"; I still have to flesh out a full proposal with all the ideas we discussed. In any case, as realms are immutable now, your proxy pattern will likely work great.

In GnoChess, an alternative approach I went for (keep in mind this was at a point where we were in full "out-ASAP" mode) was to have a "migration killswitch", which could redirect users to a new realm. This worked for GnoChess particularly as the realm path was a variable in the frontend's env file, and we could have changed that easily. Usability-wise, the proxy pattern is likely better.

The gno implementation of Flippando had its first live demo at the public Dev Call on November 8th. The demo included:

I invite you to post a separate demo video on this issue as we're a bit backlogged with publishing calls on our channel, and it might be a while before we get to publish the last call. Your demo was great & it shows another great application of Gno.land to the outside world :tada:

While taking on the task of porting math/rand, I realized that in gno we're currently missing the standard Log function in the math package. This was the only blocker in implementing basic randomness, with just one call in the NormalFloat64 distribution. I decided to create a custom implementation of the Log function, using some hard coded values and I replaced the call of Log with it. This is a hack, and it is not intended to be used in a public math.rand port. It is good enough for the level of randomness we need in the game at this moment.

This should be added by https://github.com/gnolang/gno/pull/1153 , which ports over the rest of math.

I suggest you to update https://github.com/gnolang/gno/issues/1272 with up-to-date information :)

Ideally, the rand call should be replaced once there is a stable port of the crypto/rand go library.

Do you actually mean crypto/rand?


I saw the section for your deployment scripts. With GnoChess, for a short while I used a setup which used docker + entr to re-start everything right after I made changes. This was the source code, though I imagine it won't be incredibly useful to you, as this one creates a new realm path each time.

Still, I found that getting entr to do fs watching and setting up everything for me worked wonders, and I got a nice feedback loop out of it; you may want to use it to automate your script-running, too :)

irreverentsimplicity commented 7 months ago

Thanks for the comments, really helpful.

I updated this gnolang/gno/issues/1272 with my hacky rand implementation, I understand onblock is working on this now.

And yes, I was actually meaning crypto/rand, but I don't know when / if it will be added (judging by the last dev call, it may take a while).

Separate video demo coming up in the following days. Getting ready to close this.

irreverentsimplicity commented 6 months ago

As of Dec 6th, Flippando reached a launchable state. It still can't be deployed on a testnet, though, as there is an outstanding PR on the grc721 interface.

Here are the main updates (if you want to check all the details, here's the updated board).

Finalized UI

With all the backend logic sorted out, the main focus of the last period has been around the user interface. There is now a consistent and clean theme across the entire app. The main UI elements have been simplified

Screenshot 2023-12-06 at 12 46 31 PM

Loading states and proper error checking for each backend query, empty data states enriched with CTAs to lower friction:

Screenshot 2023-12-06 at 12 46 44 PM Screenshot 2023-12-06 at 1 18 17 PM

Improved copy and UX for playground

Screenshot 2023-12-06 at 1 16 40 PM

Improved composite NFT (art) UI

Screenshot 2023-12-06 at 1 17 39 PM

Faucet integration

A faucet server has been added to the stack, to ease the onboarding of users. On testnet / mainnet, we should revisit this to set up proper limits.

Tokenomics

The $FLIP fungible token has an uncapped supply - and there will be no airdrop or premine. As long as someone solves a board and generates a basic NFT, that basic NFT will always have 1 locked $FLIP token inside. The $FLIP token can be unlocked and made really fungible, only if someone else includes that basic NFT into a composite one. So the actual liquid supply is not enforced by anything other than the players behavior.

A composite NFT can be traded for $FLIP. Each composite NFT sale also triggers a fungible token supply reduction, using randomness in a specified range. When a trade is made and the buyer pays the requested amount, a part of that amount is burned. The burnable range subject to randomness is between 1% and 50% of the asking price.

Example: seller lists a composite NFT for 10 $FLIP. A buyer agrees to pay 10 $FLIP to get that NFT. When the sale is initiated, the contract generates a random number between 1 and 50, let's say 25. In our case, 25% off of $10 FLIP means 2.5 $FLIP. This amount gets burned, and the seller gets $7.5 FLIP. Because the randomness range is between 1% and 50%, a seller can expect to receive between 99% and 50% of the asking price.

To recap: the potential supply is never capped, and it's a direct result of 1) solving boards and 2) creating art using the solved boards as basic NFTs. The selling event of a composite NFT decreases the supply with a random amount, between 1 and 50% of the sale price.

Board solvers are incentivized by the potential revenue for their painting blocks. Art collectors are incentivized by the actual designs (which are limitless) and by the fact that moving around goods decreases the $FLIP supply, hence accruing value to the fungible token. This dynamic may create in time a positive feedback loop, that will increase both engagement for NFT creation, and the value of the fungible token.

We have no way of knowing this, though, so time will tell.

Social media

There is now an official Twitter account for Flippando, feel free to follow and give feedback.

irreverentsimplicity commented 6 months ago

Here's what's been done since last update:

irreverentsimplicity commented 5 months ago

Following the merging of the grc721 PR into main gno, Flippando is now launched in beta at https://gno.flippando.xyz. It's running off of a local node, synced with the latest version of the main gno repository.

There's also a simple thread about going live on Flippando's twitter account.

Roadmap

There is also a roadmap which I will paste here with a little more dev-related details:

✅ Ground Zero (beta, testnet) - target: Jan 2024

Land of the Flips (beta, testnet) - target: March 2024

this dynamic adjustment makes the size of the composite NFTs bigger or smaller, based on how many available basic NFTs are. The more basic NFTs available, the bigger the size of the artwork, allowing for more complex and interesting compositions. If there are less than 50 available basic NFTs, the size of the canvas in playground is 2x2. Between 50 and 100, the size is 3x3, and so on. Here are the levels for this milestone:

< 50 basic NFTs: 2x2 50 -100 basic NFTs: 3x3 100 - 400 basic NFTs: 4x4 400 -600 basic NFTs: 5x5 600 - 800 basic NFTs: 6x6 800 - 1000 basic NFTs: 7x7 more than 1000 basic NFTs: 8x8

The size of the composite NFTs has very big implications on gas consumptions, making bigger compositions significantly more expensive to mint (and potentially more expensive when sold). The levels in this milestone are subject to re-evaluation.

this feature intends to surface user's games, based on gameStatus. At the moment, if a user abandons a game, there's no UI for seeing that game and potentially allowing the user to continue.

as more work was done in the ecosystem to integrate VFR (thanks Teritori), the intention is to transition to this source for randomness, instead of the hacky, simple randomness internal library used now in Flippando.

_right now all wallet interactions are done via gnojs, the intention is to use Adena for that, as it is more robust and has more features. This feature also intends to have a better faucet manegement (during testnet)

As far as I know, Gnoswap uses a registry now to list tokens in their UI. This feature intends to have a frozen realm for the FLIP fungible token, which will not be affected by future upgrades in other parts of the game, and use that as an entry point for the Gnoswap registry. This will make the GRC20 FLIP token tradeable on Gno swap. Flippando tokenomics are unusual, so it will be interesting to see how it performs with almost zero liquidity in the beginning.

this feature intends to make realms upgradeable, or at least to find a consistent way to upgrade the realms,, in the same way Solidity contracts are upgradeable.

Awakening (production, mainnet) - target: June 2024

self explanatory, we need a stable codebase to launch on mainnet

there is already a Solidity version of Flippando, deployed on a few chains, this feature intends to have Gno and Solidity at feature parity, and launched, as much as possible, at the same time.

The Flip Connection (production, mainnet, cross-chain integration) - target: September 2024