Perpetual NFTs Protocol

16 min read

An ETH Global Hackathon Project.

Create a DeFi lending Platform using NFTs as collateral, manage mint money with AAVE, and guarantee users a value! Showcase Presentation GitHub build with Polygon, AAVE, HardHat and Svelte.

I will go into technical details on how the protocol is build and explain the AAVE interactions, debt calculation and what each function does and how they relate with each other. This was build for the ETHGlobal Hackaton with my teammate 0x4non. This project is currently live HERE go mint and NFT and take a loan!

Mint, stake and earn passive income by using your NFT. On top of that, if you want to hold but use a percentage of your NFT value, take a loan! No more speculative and static NFTs! Our Protocol creates dynamic NFTs used to generate yield and take loans, all while retaining a price warranty at all times!

cool gif

Current NFT landscape:

I will quickly explain the current NFT landscape:

  • As a user you mint an NFT for a set price and all the money goes directly to the collection creator or is split by the team

  • The NFT value is purely set by the market, if there is no one that want to buy your nft then you simply lose all your investement

  • Most nft in a collection do nothing on its own, thus failing to genereate extra value

Our approach

  • On mint the value is send directly to our treasury and then is sent to AAVE wMATIC lending pool to generate yield

  • Users can donate the art to the museum and get 90% of the mint value, regardless of the market

  • Users can deposit their nfts as collateral and take up loans of up to 50% of their value , thus allowing them to use a portion of their mint money while still holding their NFTs

  • On top of that the users can liquidate positions for a reward, and AAVE yield is redistributed

Thus there is a intrinsical value for the NFTs, Yield is generated, and unlocks the possibility of loans.

Art Creation

We decided to use the neural style transfer model from TensorFlow to merge the style of popular paintings with the Polygon Network logo then, divide the results into quadrants and combine them into art pieces.

Style Transfered NFTs

Due to time constrains we did not uploaded the images to IPFS, insead they were saved locally and mapped by number.

Contract Development

We used a serie of technologies:

  • Hardhat as main dev and testing platform
  • Moralis speedy nodes for Mumbai testnet forking
  • AAVE testnet contracts
  • openZeppelin standards and waffle for testing

In total we created 3 custom contracts


Treasury: 0x0775655ab9E77DCDb3a4d08631Db192f626a19Fd

The Treasury acts as a proxy (no user info is stored in the contract) to interact with AAVE to stake the MATIC and generate value, Wrap/Unwrap MATIC, Check Native Balances, Check aWMATIC Balances and manages all the incoming/outcoming transactions, the functions are:

Contract: Treasury.sol
Sighash   |   Function Signature
d0e30db0  =>  deposit()
2e1a7d4d  =>  withdraw(uint256)
4012e02e  =>  Info()
0357371d  =>  release(address,uint256)
93d8aaa5  =>  withdrawAAVE(address,uint256)
56c545b4  =>  withdrawAAVEwMATIC(address,uint256)
f7867ca1  =>  depositAAVE()
5ad1b636  =>  wrapMATIC()
ac286bcf  =>  unwrapMATIC(uint256)
ee4ae2c9  =>  sendMoney(address,uint256)
99befcf2  =>  seeLendingPool()
973b8362  =>  aMATICbalance(address)
707752c0  =>  wMATICbalance(address)
80c9849a  =>  maticBalance(address)

To deploy, the contract must now a serie of addresses:

  // _WETHGateway 0xee9eE614Ad26963bEc1Bec0D2c92879ae1F209fA
  // _LendingPoolAddressesProviderAddress 0x178113104fEcbcD7fF8669a0150721e231F0FD4B
  // aMATIC 0xF45444171435d0aCB08a8af493837eF18e86EE27
  constructor(address _WETHGateway, address _LendingPoolAddressesProviderAddress, address _aMATIC) {
    WETHGateway = IWETHGateway(_WETHGateway);
    LendingPoolAddressesProviderAddress = _LendingPoolAddressesProviderAddress;
    aMATIC = IAToken(_aMATIC);
    wMATIC = IWMATIC(WETHGateway.getWETHAddress());
    aMATIC.approve(address(WETHGateway), type(uint).max);
    aMATIC.approve(address(this), type(uint).max);
    wMATIC.approve(address(this), type(uint).max);
  • WETHGateway, from AAVE is used when we interact with the protocol using the native currency in this case MATIC, the WETHGateaway wraps the MATIC and deposits on the LendingPool

  • LendingPoolAddressesProvider, is an immutable contract used to get the lendingPool Address

  • aMATIC aMATIC are yield-generating tokens that are minted and burned upon deposit and withdraw, AKA where the monay is generated

Treasury depositAAVE()

Deposits the msg.value amount of MATIC into AAVE, minting the same amount of corresponding aWETH, and transferring them to the Treasury address. This function is called upon token mint to sent the MATIC directly to AAVE.

  function depositAAVE() external payable {
    WETHGateway.depositETH{value: msg.value }(

*The 0 is the referal program

Treasury withdrawAAVE(address _user, uint _amount)

For some obscure reason calling the withdrawETH on the WETHGateaway did not work, we tried to solve it for 1+ hours and decided to just interact directly with the lendingPool and pass the wMATIC address. Then, unwrap the MATIC and send it to the user.

The function sends amount to user, and can only be called by the treasury owner which is Museum.sol, in were the balances checks are made

  function withdrawAAVE(address _user, uint _amount) public onlyOwner {
    IAToken(aMATIC).approve(address(WETHGateway), _amount);

    Address.sendValue(payable(_user), _amount);

A variant was also created to send the User wMATIC instead:

    function withdrawAAVEwMATIC(address _user, uint _amount) public onlyOwner {
    IAToken(aMATIC).approve(address(WETHGateway), _amount);



The museum only needs to know the NFTs and the treasury:

  constructor(address _nftToken, address payable _treasury) {
    nftToken = Perpetual(_nftToken);
    treasury = Treasury(_treasury);

The Museum manages all the Lending and Accounting for the protocol by having mappings for specific uses, some of them are:

  • collateralAmount
  • collateralNFT
  • totalNFTS
  • borrowed
  • borrowedTime

And the Museum functions:

Contract: Museum.sol
Sighash   |   Function Signature
0406bed6  =>  depositedNFTs(address)
b6b55f25  =>  deposit(uint256)
97a9d457  =>  maxBorrow(address)
1ff517ff  =>  totalDebt(address)
884719f2  =>  currentDebt(address)
2f865568  =>  liquidate(address)
8cd01307  =>  borrow(uint256,bool)
402d8883  =>  repay()
2e1a7d4d  =>  withdraw(uint256)
37bdc99b  =>  release(uint256)
6ad9f9df  =>  healthFactor(address)
511477cb  =>  nftEnumRemove(address,uint256)
150b7a02  =>  onERC721Received(address,address,uint256,bytes)

Museum deposit(uint256 _id)

User deposits a NFT, its transfered to the museum and added as collateral for the msg.sender

  function deposit(uint256 _id) external {
    nftToken.transferFrom(msg.sender, address(this), _id);
    collateralNFT[msg.sender][totalNFTS[msg.sender]] = _id;
    totalNFTS[msg.sender] += 1;

    collateralAmount[msg.sender] += nftToken.nftValue(_id);
    collateralNFTOwner[_id] = msg.sender;
    emit Deposit(msg.sender, _id);

Museum borrow(uint256 amount, bool wMATIC)

User uses his NFTs as collateral to take a loan, up to maxBorrow which is calculated taking into account the 50% borrow limit the current amount borrowed and the currentDebt (interests 10% annual) at the moment as follows:

  // user cant borrow more than 50% of its collateral
  function maxBorrow(address user) public view returns(uint256) {
    return (collateralAmount[user] / 2) - borrowed[user] - currentDebt(msg.sender);

Then their choice of Native MATIC or wMATIC is sent

  function borrow(uint256 amount, bool wMATIC) external updateDebt {

    require(amount <= maxBorrow(msg.sender), "You cant borrow more than");

    borrowed[msg.sender] += amount;

    // usamos treasury.borrowAAVE(msg.sender, amount); ??
      treasury.withdrawAAVEwMATIC(msg.sender, amount);
    } else {
      treasury.withdrawAAVE(msg.sender, amount);

    emit Borrow(msg.sender, amount);

Museum repay()

To repay the user calls the repay function with the amount they wish to pay, if the send more that they owe the contract sends them any extra money, otherwise the debt is updated. On the repayment MATIC is deposited back on AAVE

  function repay() external payable updateDebt{
    if(borrowed[msg.sender] < msg.value) {
      uint256 _changeBack = msg.value - borrowed[msg.sender];
      // AAVE deposit
      treasury.depositAAVE{value: borrowed[msg.sender]}();
      // Debt clear
      borrowed[msg.sender] = 0;
      // Send back change
      Address.sendValue(payable(msg.sender), _changeBack);
      emit Repay(msg.sender, borrowed[msg.sender]);
    } else {
      // Debt Update
      borrowed[msg.sender] -= msg.value;
      //AAVE deposit
      treasury.depositAAVE{value: msg.value}();
      emit Repay(msg.sender, msg.value);

Museum withdraw

Once the user pays their debt, or their healthFactor is not above 60% (liquidation threshold), or never took one in the first place they can withdraw their NFT from the Museum

  function withdraw(uint256 _id) external {
    require(collateralNFTOwner[_id] == msg.sender, "you are not the owner");

    collateralAmount[msg.sender] -= nftToken.nftValue(_id);
    require(healthFactor(msg.sender) < 6000, "unsafe collateral ratio");

    delete collateralNFTOwner[_id];
    nftEnumRemove(msg.sender, _id);
    nftToken.transferFrom(address(this), msg.sender, _id);
    emit Withdraw(msg.sender, _id);

Museum Liquidation(address user)

When the health factor is above 60% any user can call a liquidation over another user for FREE, this removes the collateral from the borrower and the caller is rewarded with 0.1 MATIC. The liquidated NFTs pass to be owner for the museum forever.

  function liquidate(address user) external {
    require(healthFactor(user) < 6000, "user is safe");

    for(uint i = 0; i < totalNFTS[user]; i++) {
      emit Liquidate(user, msg.sender, collateralNFT[user][i]);
      nftEnumRemove(msg.sender, collateralNFT[user][i]);
    delete collateralAmount[msg.sender];
    delete borrowed[user];
    delete borrowedTime[user];

    treasury.withdrawAAVE(msg.sender, 0.1 ether);

Debt Calculation

To update the debt a modifier function is called on every call to borrow and repay :

  modifier updateDebt() {
    // debt update
    borrowed[msg.sender] += currentDebt(msg.sender);
    borrowedTime[msg.sender] = block.timestamp;

And the debt is calulated in based on the time difference over a 10% yearly

  function currentDebt(address user) public view returns(uint256) {
    uint256 deltaT = (block.timestamp - borrowedTime[user]);
    // 10000 = 10% yearly
    // 100000 = 100%
    return (borrowed[user] * deltaT * 10000) / (365*24*60*60 * 100000);

The health factor is the user debt over his collateral

  // devuelve el healthFacto del usuario (0 -> 10000):(0 -> 100%)
  function healthFactor(address user) public view returns(uint256) {
    if(totalDebt(user) == 0) {
      return 0;
    if(collateralAmount[user] == 0) {
      return 10000;
    return totalDebt(msg.sender) * 10000 / collateralAmount[msg.sender];


And thats it! It was quite fun to work on this ETHGlobal Hackaton alongside 0x4non I learned a ton about fast paced Web3 development and switched to Svele and pure Ethers.js instead of using frameworks like scaffold-ETH and other react alternatives. I got to learn how to configure the front-end seamlessly with my contracts by listening to contracts events, and change data dynamically using svelte animations. Also really enjoyed building on top of a novel idea and interacting with other protocols!

I did not explained a lot of front-end because I am planning to create a Svelte Scaffold-ETH ! stay tunneeed

We did not end up in the top 15 but next time I will get there!


Front End

Front Page



Take Loans


Next Post

XRP NFTs Minting and Trading Dapp

21 min read

For this project I made use of the XLS-20 Standard preliminary implementation in the XRP Ledger protocol to create a dapp for NFT minting, management and trading. Build with XRPL, MongoDB and Svelte.