Perpetual NFTs Protocol
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!
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.
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.sol
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 }(
seeLendingPool(),
address(this),
0
);
}
*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);
ILendingPool(seeLendingPool()).withdraw(
address(wMATIC),
_amount,
address(this)
);
wMATIC.withdraw(_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);
ILendingPool(seeLendingPool()).withdraw(
address(wMATIC),
_amount,
_user
);
}
Museum.sol
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); ??
if(wMATIC){
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];
}
Conclusion
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!
LIVE POLYGON MUMBAI TESTNET CONTRACTS:
perpetual: 0x1830e4E3F11910FcA52e9E1d891B61C87998D84c
Live Page Go mint one and take a loan!