Generalised collateral-free and permissionless rentals built on top of Gnosis Safe and Seaport.
Running all unit tests can be done with Foundry:
forge test -vvv
We use slither to run static analysis. Make sure it is installed via:
pip3 install slither-analyzer
To run the static analysis on the contracts:
slither .
Before deploying, first make sure that all relevant API keys are set in the .env
file.
You can reference the .env.example
as a template for properly formatting the environment variables.
Supported chains for deployment include:
mainnet
: ETH L1 mainnetsepolia
: ETH L1 testnetpolygon
: Polygon mainnetmumbai
: Polygon testnet
You can simulate the deployment to get a sense of the gas cost and to double check the output to make sure the right deployer and server signer addresses are being used:
make simulate-deploy script=DeployProtocol chain=mainnet gas-price=40000000000
This command simulates a mainnet deploy with a max fee per gas of 40 gwei. You will get an output that looks something like this:
Estimated gas price: 40 gwei
Estimated total gas used for script: 38516728
Estimated amount required: 1.54066912 ETH
If everything looks good and the deployer wallet has enough to cover the costs at estimated
prices, then you can execute the actual deployment by swapping simulate-deploy
with deploy
:
make deploy script=DeployProtocol chain=mainnet gas-price=40000000000
It will take a bit of time to execute the deployment, and the scripts will automatically start verifying each contract. It is crucial that the block explorer API keys are set up properly. If the verification fails because of an invalid key, then it will have to be done manually which is a very tedious process.
- Recipient of ERC721 / ERC1155 tokens is always the reNFT smart contract renter wallet
- Recipient of ERC20 tokens is always the Payment Escrow Module
- Stored token balance of the
src/modules/PaymentEscrow.sol
contract should never be less than the true token balance of the contract - Rental safes can never make a call to
setGuard()
- Rental safes can never make a call to
enableModule()
ordisableModule
unless the target has been whitelisted bysrc/policies/Admin.sol
- Rental safes can never make a delegatecall unless the target has been
whitelisted by
src/policies/Admin.sol
- ERC721 / ERC1155 tokens cannot leave a rental wallet via
approve()
,setApprovalForAll()
,safeTransferFrom()
,transferFrom()
, orsafeBatchTransferFrom()
- Hooks can be specified for ERC721 and ERC1155 items
- Only one hook can act as middleware to a target contract at one time. But, there is no limit on the amount of hooks that can execute during rental start or stop.
- When control flow is passed to hook contracts, the rental concerning the hook
will be active and a record of it will be stored in
src/modules/Storage.sol
When signing a rental order, the lender can decide to include an array of Hook
structs along with it. These are bespoke restrictions or added functionality
that can be applied to the rented token within the wallet. This protocol allows
for flexibility in how these hooks are implemented and what they restrict. A
common use-case for a hook is to prevent a call to a specific function selector
on a contract when renting a particular token ID from an ERC721/ERC1155
collection.
Adding a hook contract to the protocol is an admin-permissioned action on the
src/policies/Guard.sol
contract which is done via:
updateHookStatus()
which enables a hook for use within the protocol.
updateHookPath()
which specifies the contract which the rental wallet
interacts with that will activate the hook.
When creating a rental, a OrderMetadata
struct will be added to the order
which specifies extra parameters to pass along with the rentals:
struct OrderMetadata {
// the type of order being created
OrderType orderType;
// the duration of the rental in seconds
uint256 rentDuration;
// the hooks that will act as middleware for the items in the order
Hook[] hooks;
// any extra data to be emitted upon order fulfillment
bytes emittedExtraData;
}
Hooks can be added here to specify the unique functionality placed upon tokens
in the order. Only hooks which have been enabled by the admin will be valid when
passed to the address target
field.
struct Hook {
// the hook contract to target
address target;
// index of the item in the order to apply the hook to
uint256 itemIndex;
// any extra data that the hook will need. This will most likely
// be some type of bitmap scheme
bytes extraData;
}
After a renter has successfully rented an ERC721/ERC1155, the
src/policies/Guard.sol
contract will be invoked each time a transaction
originates from the wallet. The contract will check its mapping for any hooks in
the path of the interacting address.
If a hook exists, the control flow will be handed over to the hook contract for further processing.
If no hook address was found, the rental guard contract contains basic restrictions that prevents the usage of ERC721/ERC1155 state-changing functions.
Example implementations of hooks can be found in the
src/examples/restricted-selector/
folder.
Per each erc721 GameToken
ID, this hook uses a bitmap which tracks any
function selectors that are restricted for that token ID only. Bitmaps allow
support for up to 256 function selectors on a single contract.
Using a token ID -> bitmap
mapping allows the property that 2 or more tokens
from the same collection can be restricted in different ways based on how their
lenders defined the permissions.
Hooks are extendable. An allowlisted hook for a collection can be expanded even further to allow for multiple child hooks that are routed to based on logic defined in the parent hook. This pattern enables granular control flow of the transaction execution for any requirements or restrictions that a rental may have.
Transactions that leverage delegate call on the safe contract are not allowed.
This invariant helps protect the safe from a few attack vectors. If delegate
call were allowed, a transaction could be crafted that would allow an
approve()
call on a rented NFT even though the guard contract disables calls
to approve()
. See test/adversarial/DelegateCallApproveAttack
for more info
on this. Also, allowing delegate call would introduce another attack surface via
the re-initialization of the safe. Because the safe uses delegate call on
instantiation to enable the module and guard contract, the
initializeRentalSafe()
function could be delegate called again after
instantiation and overwrite the active guard or enable a new module.
To mitigate this attack vector while still allowing delegate call, the contracts use a whitelist to allow delegate call to specific addresses that are deemed safe. For example, a contract dedicated to updating a gnosis safe module to a newer version could be whitelisted by the protocol to allow a rental safe to call it.
The src/policies/Guard.sol
contract can only protect against the transfer of
tokens that faithfully implement the ERC721/ERC1155 spec. A dishonest
implementation that adds an additional function to transfer the token to another
wallet cannot be prevented by the protocol.
The protocol contracts do not expect to be interacting with any ERC20 token
balances that can change during transfer due to a fee, or change balance while
owned by the src/modules/PaymentEscrow.sol
contract.