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:
- The frontend constructs a CosmWasm
ExecuteMsgas JSON. - The frontend UTF-8 encodes that JSON to
bytes. - The Solidity gateway forwards those bytes to the Nibiru Wasm precompile (
IWasm.execute). - 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
- Repository layout
- Core dependencies (Nibiru precompiles)
- Contract:
CodedEstateEvmInterface - Action catalog (Rust variant -> JSON -> Solidity call)
- TypeScript support (
evmImpl.ts)
1 - Install bun.
2 - Run nvm use.
3 - Install just to run project-specific commands.
cargo install justCompile Solidity contracts.
just buildcontracts/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.
This interface relies on Nibiru's precompiled contracts:
IWasm(WASM_PRECOMPILE_ADDRESS = 0x...0802)- Executes a Wasm contract's
ExecuteMsggiven JSON bytes and optional funds.
- Executes a Wasm contract's
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.
CodedEstateEvmInterface is a thin wrapper around IWasm.execute, plus a convenience function for "reservation with payment".
rentalContractAddress(string, bech32)- The current Wasm contract address that receives
ExecuteMsg.
- The current Wasm contract address that receives
- Timelocked address update fields:
timelockDelaypendingRentalContractAddresschangeEffectiveTimestamp
The owner can rotate the underlying Wasm rental contract address with a delay:
proposeNewRentalContractAddress(string newAddress)- Sets
pendingRentalContractAddressand schedules an effective timestamp.
- Sets
executeNewRentalContractAddress()- After the timelock expires, makes the new address active.
This pattern mirrors the PerpVault EVM interface pattern.
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)Use this for actions that do not require payment:
executeRental(bytes wasmMsgExecute)
Use this when the Wasm contract expects bank-denom funds:
executeRentalWithFunds(bytes wasmMsgExecute, BankCoin[] funds)
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 callssendToBank(paymentErc20, useErc20Amount, callerBech32). - If
paymentDenomis empty, it derives the bank denom frompaymentErc20. - Then it calls
executeRentalWithFundswith exactly oneBankCoin { denom, amount }.
This keeps the Wasm message payload focused on rental logic while the gateway handles payment mechanics.
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
- 2) List property for short-term rental
- 3) Unlist property
- 4) Create reservation (requires funds)
- 5) Approve reservation
- 6) Reject reservation (opaque)
- 7) Cancel reservation (unapproved, opaque)
- 8) Cancel rental (approved, opaque)
- 9) Finalize rental (opaque)
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)
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)
Rust variant (expected): ExecuteMsg::SetUnlistForShorttermRental { token_id }
Wire JSON
{ "set_unlist_for_shortterm_rental": { "token_id": "prop_123" } }Solidity call
executeRental(wasmMsgBytes)
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
)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)
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)
Wire key: cancel_reservation_for_shortterm
Same situation as above:
executeRental(wasmMsgBytes)
Wire key: cancel_rental_for_shortterm
Same situation:
executeRental(wasmMsgBytes)
Wire key: finalize_short_term_rental
Same situation:
executeRental(wasmMsgBytes)
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.