Skip to content

NibiruChain/coded-estate-evm

Repository files navigation

CodedEstate EVM Interface

This repo contains a Solidity "gateway" contract that lets EVM callers execute CodedEstate rental actions implemented as a CosmWasm contract on Nibiru.

At a high level:

  1. The frontend constructs a CosmWasm ExecuteMsg as JSON.
  2. The frontend UTF-8 encodes that JSON to bytes.
  3. The Solidity gateway forwards those bytes to the Nibiru Wasm precompile (IWasm.execute).
  4. For actions that require payment, the gateway can (optionally) convert ERC20 -> bank coins using the FunToken precompile, then attach bank-denom funds to the Wasm execution.

The result is a stable EVM ABI while the Wasm contract evolves. Most functions accept bytes wasmMsgExecute so the schema stays on the Wasm side.

Building the Contracts

1 - Install bun.

2 - Run nvm use.

3 - Install just to run project-specific commands.

cargo install just

Compile Solidity contracts.

just build

Ref: github.com/casey/just

Repository layout

  • contracts/CodedEstateEvmInterface.sol
    • The gateway contract deployed on Nibiru EVM.
  • evmImpl.ts
    • TypeScript types for the known JSON message shapes and a helper for encoding messages.
  • hardhat.config.js, package.json, justfile
    • Build tooling for compiling the Solidity contract and generating artifacts.

Core dependencies (Nibiru precompiles)

This interface relies on Nibiru's precompiled contracts:

  • IWasm (WASM_PRECOMPILE_ADDRESS = 0x...0802)
    • Executes a Wasm contract's ExecuteMsg given JSON bytes and optional funds.
  • IFunToken (FUNTOKEN_PRECOMPILE_ADDRESS = 0x...0800)
    • Resolves EVM <-> bech32 address pairs and converts ERC20 <-> bank-denom balances.

You do not call these directly from the frontend. The gateway delegates to them internally.


Contract: CodedEstateEvmInterface

CodedEstateEvmInterface is a thin wrapper around IWasm.execute, plus a convenience function for "reservation with payment".

What the contract stores

  • rentalContractAddress (string, bech32)
    • The current Wasm contract address that receives ExecuteMsg.
  • Timelocked address update fields:
    • timelockDelay
    • pendingRentalContractAddress
    • changeEffectiveTimestamp

Timelocked address updates

The owner can rotate the underlying Wasm rental contract address with a delay:

  • proposeNewRentalContractAddress(string newAddress)
    • Sets pendingRentalContractAddress and schedules an effective timestamp.
  • executeNewRentalContractAddress()
    • After the timelock expires, makes the new address active.

This pattern mirrors the PerpVault EVM interface pattern.

Calling pattern

Encode a Wasm execute message

Every CodedEstate action is expressed as a JSON object with a single top-level key (the execute message name) and a payload object.

Encode it like this:

import { ethers } from "ethers"

const wasmMsg = { /* ... */ }
const wasmMsgBytes = ethers.toUtf8Bytes(JSON.stringify(wasmMsg))

Or use the helper in evmImpl.ts:

import { encodeRentalMessage } from "./evmImpl"

const wasmMsgBytes = encodeRentalMessage(wasmMsg)

Execute without funds

Use this for actions that do not require payment:

  • executeRental(bytes wasmMsgExecute)

Execute with funds

Use this when the Wasm contract expects bank-denom funds:

  • executeRentalWithFunds(bytes wasmMsgExecute, BankCoin[] funds)

Reservation with payment (special wrapper)

Reservations require payment to be attached as funds. Use:

  • setReservationForShortTerm( bytes wasmMsgExecute, string paymentDenom, uint256 paymentAmountBank, address paymentErc20, uint256 useErc20Amount )

What it does:

  • Requires paymentAmountBank > 0.
  • If useErc20Amount > 0, it calls sendToBank(paymentErc20, useErc20Amount, callerBech32).
  • If paymentDenom is empty, it derives the bank denom from paymentErc20.
  • Then it calls executeRentalWithFunds with exactly one BankCoin { denom, amount }.

This keeps the Wasm message payload focused on rental logic while the gateway handles payment mechanics.


Action catalog (Rust variant -> JSON -> Solidity call)

Below, "Rust variant" names are the expected ExecuteMsg variants based on the README naming convention (#[serde(rename_all = "snake_case")]). The wire truth is the JSON key.

1) Mint NFT

Rust variant (expected): ExecuteMsg::Mint { token_id, owner, token_uri, extension }

Wire JSON

{
  "mint": {
    "token_id": "prop_123",
    "owner": "nibi1...",
    "token_uri": "ipfs://cid",
    "extension": {}
  }
}

Solidity call

  • executeRental(wasmMsgBytes)

2) List property for short-term rental

Rust variant (expected): ExecuteMsg::SetListForShortTermRental { ... }

Wire JSON

{
  "set_list_for_short_term_rental": {
    "token_id": "prop_123",
    "denom": "uusdc",
    "price_per_day": "2500000",
    "auto_approve": false,
    "available_period": [],
    "minimum_stay": "2",
    "cancellation": [{ "deadline": "86400", "percentage": "50" }]
  }
}

Solidity call

  • executeRental(wasmMsgBytes)

3) Unlist property

Rust variant (expected): ExecuteMsg::SetUnlistForShorttermRental { token_id }

Wire JSON

{ "set_unlist_for_shortterm_rental": { "token_id": "prop_123" } }

Solidity call

  • executeRental(wasmMsgBytes)

4) Create reservation (requires funds)

Rust variant (expected): ExecuteMsg::SetReservationForShortTerm { token_id, renting_period, guests }

Wire JSON

{
  "set_reservation_for_short_term": {
    "token_id": "prop_123",
    "renting_period": ["1706745600", "1707004800"],
    "guests": "2"
  }
}

Solidity call

Use setReservationForShortTerm(...) so the gateway attaches payment funds.

Example:

await codedEstateEvm.setReservationForShortTerm(
  wasmMsgBytes,
  "uusdc",
  5_000_000n,         // bank units sent as funds
  ethers.ZeroAddress,  // paymentErc20 (optional)
  0n,                  // useErc20Amount (optional)
  overrides
)

If paying via ERC20 first:

await codedEstateEvm.setReservationForShortTerm(
  wasmMsgBytes,
  "uusdc",            // or "" to derive from paymentErc20
  5_000_000n,
  usdcErc20Address,
  useErc20Amount,
  overrides
)

5) Approve reservation

Rust variant (expected): ExecuteMsg::SetApproveForShortTerm { token_id, traveler, renting_period }

Wire JSON

{
  "set_approve_for_short_term": {
    "token_id": "prop_123",
    "traveler": "nibi1...",
    "renting_period": ["1706745600", "1707004800"]
  }
}

Solidity call

  • executeRental(wasmMsgBytes)

6) Reject reservation (opaque)

Wire key: reject_reservation_for_shortterm

The README names this message but does not specify field-level JSON. Keep the payload opaque and send it through:

  • executeRental(wasmMsgBytes)

7) Cancel reservation (unapproved, opaque)

Wire key: cancel_reservation_for_shortterm

Same situation as above:

  • executeRental(wasmMsgBytes)

8) Cancel rental (approved, opaque)

Wire key: cancel_rental_for_shortterm

Same situation:

  • executeRental(wasmMsgBytes)

9) Finalize rental (opaque)

Wire key: finalize_short_term_rental

Same situation:

  • executeRental(wasmMsgBytes)

TypeScript support (evmImpl.ts)

evmImpl.ts provides:

  • Primitive types (bech32, ipfs URIs, string-encoded integers).
  • Strict types for message shapes that are explicitly defined in the README.
  • Opaque placeholders for message names that exist but lack field-level schema.
  • encodeRentalMessage(msg) helper: ethers.toUtf8Bytes(JSON.stringify(msg)).

The intent is: type what we know, avoid guessing what we do not.

About

Solidity and EVM interface contracts for Coded Estate rental app

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Contributors