Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

30 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

RedDuck

Proxy Wallet — Contracts

A smart-contract wallet that can be rented, not just owned — Chainlink Automation guarantees the asset always comes back.


A Solidity + Hardhat implementation of a rentable smart-contract wallet: a minimal-proxy wallet that can hold an NFT (or ownership of another contract), be temporarily controlled by a renter, and guarantee — without trusting the renter — that it returns to the original owner when the rental period ends. This repository holds the on-chain half of the Proxy Wallet project; chainlink-hackathon-web-wallet is the browser-extension wallet and rental marketplace UI that drives these contracts, and chainlink-hackathon-subgraph indexes their events for both to query.

Built with

Area Technology
Contracts Solidity 0.8.23–0.8.24, OpenZeppelin Contracts & Contracts Upgradeable
Automation & liquidity Chainlink Automation (custom logic upkeep), LINK token, Uniswap V3 (ETH → LINK swaps)
Tooling Hardhat, Hardhat Ignition (declarative deployments), @nomicfoundation/hardhat-toolbox-viem, Viem, TypeScript
Testing Hardhat + Chai — fixture-based unit tests
Analysis Slither, Solhint, ESLint, Prettier, hardhat-contract-sizer
Config dotenv, a vendored @t3-oss/env-style validated env (config/create-env.ts)

The mechanisms, and why each exists

Renting an on-chain asset has one hard problem: the renter needs real control over it, but the owner needs a guarantee it comes back. Everything here exists to answer one of four practical questions that guarantee raises:

Question Mechanism Contract
How do you hand a renter real control without handing them the asset? Deploy a disposable, per-rental smart wallet that holds the asset on the renter's behalf SmartWalletV1.sol
How do you stop that wallet's owner from just transferring the asset away? A per-selector blacklist, checked on every call the wallet makes SmartWalletV1.sol (blacklistedFunctions)
How do you guarantee the asset returns even if the renter goes silent? A Chainlink Automation upkeep scheduled at rental setup, executable by anyone as a fallback SmartWalletV1.sol (addToAutoExecute / checkUpkeep / performUpkeep)
How do you make deploying one wallet per rental cheap enough to matter? Deterministic EIP-1167 minimal-proxy clones SmartWalletFactoryV1.sol

Architecture

contracts/
├── SmartWalletV1.sol             # The rentable wallet: allowlist, blacklist, Chainlink Automation auto-execute
├── SmartWalletFactoryV1.sol      # Deploys SmartWalletV1 as deterministic minimal-proxy clones (EIP-1167)
├── integration/
│   ├── NftRent.sol                 # NFT rental marketplace: escrows the NFT, configures the wallet, verifies returns
│   └── NftRentWithPermit.sol        # NftRent + an EIP-712 permit, so renting is a single transaction
├── interfaces/
│   ├── ISmartWallet.sol             # Wallet's external surface (allowlist, blacklist, auto-execute)
│   ├── ISmartWalletFactory.sol      # Factory's external surface + CreateWalletParams
│   ├── IAutoExecuteCallback.sol     # Callback the wallet invokes once a scheduled auto-execute completes
│   ├── IAutomationCompatible.sol    # Chainlink Automation's checkUpkeep / performUpkeep interface
│   ├── IAutomationRegistrarInterface.sol / IAutomationRegistryInterface.sol  # Upkeep registration & funding
│   ├── ILinkPegSwap.sol             # Optional LINK bridging swap (network-dependent)
│   └── IUniswapRouterV3.sol / IWeth.sol  # ETH → LINK swap path used to self-fund the upkeep
├── libraries/
│   ├── EnumerableMap.sol            # uint256 -> AutoExecute map, backing the wallet's schedule
│   └── UniswapV3Actions.sol         # exactOutput swap helper used to top up LINK
└── test/
    └── TestERC721.sol               # Minimal ERC-721 used by tests and local deployments

Rental lifecycle

stateDiagram-v2
    [*] --> Listed: list()
    Listed --> Rented: rent() / rentExternal()
    Rented --> Returned: returnRented() — renter returns early
    Rented --> Returned: Chainlink Automation performUpkeep() — deadline reached
    Returned --> [*]
Loading

Rented isn't a trust-based state: from the moment rent() configures the wallet, the asset's transfer functions are blacklisted on that wallet and a return transaction is already scheduled with Chainlink Automation. Nothing further needs to happen for the guarantee to hold — returnRented() only exists so a cooperative renter can settle early instead of waiting for the scheduled upkeep.

Wallet configuration flow

flowchart LR
    Lister -->|list NFT| NftRent
    Renter -->|rent + fee| NftRent
    NftRent -->|create2Wallet| SmartWalletFactoryV1
    SmartWalletFactoryV1 -->|deploys clone| SmartWalletV1
    NftRent -->|blacklist transfer selectors + addToAutoExecute| SmartWalletV1
    SmartWalletV1 -->|registers / funds upkeep| Automation[Chainlink Automation]
    Automation -->|performUpkeep at rentEndsAt| SmartWalletV1
    SmartWalletV1 -->|safeTransferFrom back to lister| NftRent
Loading

SmartWalletV1 never talks to NftRent directly once configured — it only knows it has a blacklist entry and a scheduled AutoExecute. This is what lets the same wallet be reused, in principle, by any integration that wants "hand over controlled access, guarantee it comes back": NftRent is one such integration, calling blacklist() and addToAutoExecute() through ISmartWallet, not through any NFT-specific code path in the wallet itself.

Renting and returning

  1. list(tokenContract, tokenId, rentDuration, ethFee) — the owner escrows the NFT in NftRent and publishes rental terms.
  2. rent(id) — a renter pays ethFee. NftRent deploys them a fresh SmartWalletV1 clone via SmartWalletFactoryV1, transfers the NFT into it, blacklists the NFT's approve / setApprovalForAll / transferFrom / safeTransferFrom selectors on that wallet, and schedules a safeTransferFrom back to the lister at rentEndsAt — with NftRent registered as the completion callback. rentExternal(id) is the same flow for a renter who already owns a SmartWalletV1 (verified via SmartWalletFactoryV1.validateWallet), skipping the deploy step; NftRentWithPermit.rentExternalWithPermit() folds the wallet's allowlist approval into that same transaction via an EIP-712 signature, so no separate approval transaction is needed first.
  3. Settlement — either the renter calls returnRented() to settle early, or Chainlink Automation's performUpkeep() fires once rentEndsAt has passed and calls back into NftRent.autoExecuteCallback(). Either path removes the blacklist entries and the wallet's allowlist grant for NftRent, leaving a clean wallet behind.

The wallet funds its own upkeep: _fundClUpkeep() swaps ETH held by the wallet into LINK through Uniswap V3 (and, on chains where native LINK isn't the Automation-compatible token, through a peg-swap), then registers or tops up the Chainlink Automation upkeep — a renter never needs to hold LINK themselves.

Getting started

npm ci
npx hardhat compile
npx hardhat test

Copy .env.example to .env and fill in an Infura key and mnemonics before touching a live network:

INFURA_KEY="<string>"
MNEMONIC_DEV="<string>"
MNEMONIC_PROD="<string>"
FORKING_NETWORK="<main | sepolia>"
ETHERSCAN_API_KEY="<string>"

Deploy with Hardhat Ignition, passing the network-specific Chainlink/Uniswap addresses each module needs (see ignition/parameters/<network>.json):

npm run deploy ./ignition/modules/SmartWalletFactoryV1.ts -- --parameters ./ignition/parameters/sepolia.json --network sepolia
npm run deploy ./ignition/modules/NftRent.ts -- --parameters ./ignition/parameters/sepolia.json --network sepolia

scripts/deploy_factory.ts and scripts/deploy_nftrent.ts wrap the same modules for a scripted deploy. Deployed addresses land in ignition/deployments/<chainId>/deployed_addresses.json — the web-wallet's rent-app reads its own copy of these addresses from packages/rent-app/src/constants/addresses.ts, and the subgraph reads its nftRent address from chainlink-hackathon-subgraph/config/<network>.json; keep the three in sync after a redeploy.

Run the static analyzers before opening a PR:

npm run codestyle   # lint:sol, lint:ts, format:sol, format:ts
npm run slither      # Slither static analysis

Design notes & trade-offs

  • Minimal proxy clones over full deploys. Every rental gets its own SmartWalletV1, so wallet-creation gas has to be cheap — EIP-1167 clones make a fresh wallet a few thousand gas instead of a full contract deployment.
  • Blacklist a selector, not an address. blacklistedFunctions[to][selector] blocks only the specific calls that would move the asset (approve, transferFrom, …) on the specific token contract — the wallet stays usable for everything else during the rental.
  • Cancellation is creator-gated. removeAutoExecute / executeRevert require msg.sender == data.creator, i.e. only NftRent (or whoever scheduled the auto-execute) can cancel it — the wallet's own owner cannot unilaterally cancel their own scheduled return.
  • Self-funding upkeep. Requiring renters to pre-fund LINK would be a real UX tax; swapping the wallet's own ETH through Uniswap V3 on demand removes that step entirely.
  • Permit-based allowlisting. addToAllowlistWithPermit lets a repeat renter authorize NftRent with a signature instead of a separate on-chain transaction, collapsing "approve, then rent" into one call.
  • Public fallback execution. performUpkeep and executeRevert are callable by anyone once their time condition is met — if Chainlink Automation itself were ever unavailable, the return can still be forced through.

Testing

test/SmartWalletV1.test.ts covers wallet creation, the allowlist/blacklist gates, addToAutoExecute / performUpkeep scheduling, and blacklist enforcement against a live fork fixture. Reading it top-to-bottom is a good way to see how the pieces above compose in practice.

License

GPL-3.0

About

Rentable smart-contract wallet secured by Chainlink Automation.

Resources

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages