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.
| 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) |
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 |
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
stateDiagram-v2
[*] --> Listed: list()
Listed --> Rented: rent() / rentExternal()
Rented --> Returned: returnRented() — renter returns early
Rented --> Returned: Chainlink Automation performUpkeep() — deadline reached
Returned --> [*]
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.
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
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.
list(tokenContract, tokenId, rentDuration, ethFee)— the owner escrows the NFT inNftRentand publishes rental terms.rent(id)— a renter paysethFee.NftRentdeploys them a freshSmartWalletV1clone viaSmartWalletFactoryV1, transfers the NFT into it, blacklists the NFT'sapprove/setApprovalForAll/transferFrom/safeTransferFromselectors on that wallet, and schedules asafeTransferFromback to the lister atrentEndsAt— withNftRentregistered as the completion callback.rentExternal(id)is the same flow for a renter who already owns aSmartWalletV1(verified viaSmartWalletFactoryV1.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.- Settlement — either the renter calls
returnRented()to settle early, or Chainlink Automation'sperformUpkeep()fires oncerentEndsAthas passed and calls back intoNftRent.autoExecuteCallback(). Either path removes the blacklist entries and the wallet's allowlist grant forNftRent, 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.
npm ci
npx hardhat compile
npx hardhat testCopy .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 sepoliascripts/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- 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/executeRevertrequiremsg.sender == data.creator, i.e. onlyNftRent(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.
addToAllowlistWithPermitlets a repeat renter authorizeNftRentwith a signature instead of a separate on-chain transaction, collapsing "approve, then rent" into one call. - Public fallback execution.
performUpkeepandexecuteRevertare callable by anyone once their time condition is met — if Chainlink Automation itself were ever unavailable, the return can still be forced through.
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.