Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions contracts/interfaces/INFTCollateralProvider.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.16;

// We are extending the interface for ERC-721 tokens
import './ICollateralProvider.sol';

interface INFTCollateralProvider is ICollateralProvider {
function deposit(uint256 keyId, uint256 tokenId, address nftContractAddress) external;

function withdrawal(uint256 keyId, uint256 tokenId) external;
}
133 changes: 133 additions & 0 deletions contracts/providers/NFTVault.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.16;

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";

// Be able to produce the ethereum arn
import "../../libraries/AssetResourceName.sol";
using AssetResourceName for AssetResourceName.AssetType;

// We want to track contract addresses
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
using EnumerableSet for EnumerableSet.AddressSet;


import "../interfaces/IKeyVault.sol";
import "../interfaces/ILocksmith.sol";
import "../interfaces/ILedger.sol";
import "../interfaces/INFTCollateralProvider.sol";

contract NFTVault is
INFTCollateralProvider,
Initializable,
OwnableUpgradeable,
UUPSUpgradeable
{
IERC721 public nftContract;

///////////////////////////////////////////////////////
// Storage
///////////////////////////////////////////////////////
// Locksmith verifies key-holdership.
ILocksmith public locksmith;

// The Locksmith provides access to mutate the ledger.
ILedger public ledger;

// witnessed token addresses
// trust => [registered addresses]
mapping(uint256 => EnumerableSet.AddressSet)
private witnessedTokenAddresses;
mapping(bytes32 => address) public arnContracts;

// we need to keep track of the deposit balances safely
mapping(address => uint256) nftBalances;



/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
// this disables all previous initializers
_disableInitializers();
}

function initialize(
address _Locksmith,
address _Ledger
) initializer public {
__Ownable_init();
__UUPSUpgradeable_init();

// this implies a specific deployment order that trust key
// must be mined first.
locksmith = ILocksmith(_Locksmith);
ledger = ILedger(_Ledger);
}

function _authorizeUpgrade(address) internal view override onlyOwner {}

function getTrustedLedger() external view returns (address) {
return address(ledger);
}


// TODO: add comments
function deposit(
uint256 keyId,
uint256 tokenId,
address nftContractAddress
) external {
// stop right now if the message sender doesn't hold the key
require(locksmith.hasKeyOrTrustRoot(msg.sender, keyId), "KEY_NOT_HELD");

// generate the nft ARN
bytes32 nftARN = AssetResourceName
.AssetType({
contractAddress: nftContractAddress,
tokenStandard: 721,
id: tokenId
})
.arn();

// increment the witnessed token balance
nftBalances[nftContractAddress] += 1;

(, , uint256 trustId, , ) = locksmith.inspectKey(keyId);
witnessedTokenAddresses[trustId].add(nftContractAddress);

// Transfer the NFT to the vault
// TODO: implement safeTransferFrom once IERC721Receiver is implemented
IERC721(nftContractAddress).transferFrom(
msg.sender,
address(this),
tokenId
);

// track the deposit on the ledger
// this could revert for a few reasons:
// - the key is not root
// - the vault is not a trusted collateral provider the ledger
(, , uint256 finalLedgerBalance) = ledger.deposit(keyId, nftARN, 1);

// jam the vault if the ledger's balance
// provisions doesn't match the vault balance
assert(finalLedgerBalance == nftBalances[nftContractAddress]);
}

function withdrawal(uint256 keyId, uint256 tokenId) external {
// Transfer the NFT from the vault to the owner
nftContract.transferFrom(address(this), owner(), tokenId);
}

function arnWithdrawal(
uint256 keyId,
bytes32 arn,
uint256 amount
) external override {}


}
13 changes: 13 additions & 0 deletions contracts/stubs/ShadowNFT.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.16;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";

contract ShadowNFT is ERC721 {
constructor(string memory name, string memory symbol) ERC721(name, symbol) {
}

function mint(uint256 tokenId) public {
_mint(msg.sender, tokenId);
}
}
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"name": "smartrust",
"dependencies": {
"@ledgerhq/hw-app-eth": "6.33.6",
"@nomicfoundation/hardhat-toolbox": "^1.0.2",
"@openzeppelin/contracts": "^4.7.3",
"@openzeppelin/contracts-upgradeable": "^4.7.3",
"@openzeppelin/contracts": "4.7.3",
"@openzeppelin/contracts-upgradeable": "4.7.3",
"dotenv": "^16.0.3",
"hardhat": "^2.12.1-ir.0",
"hardhat-contract-sizer": "^2.6.1",
"@ledgerhq/hw-app-eth": "6.33.6"
"hardhat": "^2.19.0",
"hardhat-contract-sizer": "^2.6.1"
},
"devDependencies": {
"@nomicfoundation/hardhat-chai-matchers": "^1.0.6",
Expand Down
115 changes: 115 additions & 0 deletions test/NftVault.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
const { expect } = require("chai"); // used for assertions
const {
loadFixture // used for test setup
} = require("@nomicfoundation/hardhat-network-helpers");
require('./TrustTestUtils.js');


describe("NFTVault", function () {
describe("Contract deployment", function () {
it("Should not fail the deployment", async function () {
const { locksmith, ledger, nftVault } = await loadFixture(TrustTestFixtures.freshNFTVault);

expect(await nftVault.locksmith()).to.equal(locksmith.address);
expect(await nftVault.ledger()).to.equal(ledger.address);

await expect(nftVault.initialize(locksmith.address, ledger.address))
.to.be.revertedWith("Initializable: contract is already initialized");
});

it("Should have no coin balance", async function () {
const { locksmith, ledger, nftVault, nft} = await loadFixture(TrustTestFixtures.freshNFTVault);
expect(await nft.balanceOf(nftVault.address)).to.equal(0); // checks the contract balance
});
});


////////////////////////////////////////////////////////////
// Deposit ERC721
//
// This test suite should test our ability to create trusts,
// and deposit ERC721.
////////////////////////////////////////////////////////////
describe("Basic Deposit Use Cases", function () {
it("Happy Case Deposit Sanity", async function() {
const { keyVault, locksmith,
notary, ledger, nftVault, nft,
owner, root, second, third } = await loadFixture(TrustTestFixtures.freshNFTVault);

expect(await nft.balanceOf(root.address), 1);
expect(await nft.balanceOf(second.address), 1);
expect(await nft.balanceOf(third.address), 1);

expect(root.address, await nft.ownerOf(1))
expect(second.address, await nft.ownerOf(2))
expect(third.address, await nft.ownerOf(3))

// create a second trust with a different owner
await locksmith.connect(second).createTrustAndRootKey(stb("Second Trust"), second.address);

// create a secondary key on that trust
await locksmith.connect(second).createKey(1, stb('2'), third.address, false);

// 2nd owner is trying to access NFT without trusted permission
await expect(nftVault.connect(second)
.deposit(1, 2, nft.address))
.to.be.revertedWith('UNTRUSTED_ACTOR');

await notary.connect(second).setTrustedLedgerRole(1, 0, ledger.address, nftVault.address, true, stb('nft Vault'));

// 2nd owner is trying to access NFT with trusted permission
await expect(await nftVault.connect(second).deposit(1, 2, nft.address))
.to.emit(ledger, "depositOccurred");

// check all the balances of the accounts once more
expect(await nft.balanceOf(root.address), 1);
expect(await nft.balanceOf(second.address), 0); // this changed
expect(await nft.balanceOf(third.address), 1);

// check the balance of the ERC721 for the entire trust contract,
// check that the nftVault actually has recieved the nft and is the owner.
expect(await nft.balanceOf(nftVault.address)).to.equal(1);
expect(nftVault.address, await nft.ownerOf(2))

});

it("Require to hold key used for deposit", async function() {
const { keyVault, locksmith,
notary, ledger, nftVault, nft,
owner, root, second, third } = await loadFixture(TrustTestFixtures.freshNFTVault);

// try to deposit without a key
await expect(nftVault.connect(second).deposit(0, 2, nft.address))
.to.be.revertedWith('KEY_NOT_HELD');
});

it("Does not have deposit permission", async function() {
const { keyVault, locksmith,
notary, ledger, nftVault, nft,
owner, root, second, third } = await loadFixture(TrustTestFixtures.freshNFTVault);

// mint a different root
await locksmith.connect(root).createTrustAndRootKey(stb('my-trust'), second.address)
expect(await keyVault.balanceOf(second.address, 1)).to.equal(1);

// try to deposit as a beneficiary, and fail
await expect(nftVault.connect(second)
.deposit(1, 2, nft.address))
.to.be.revertedWith("UNTRUSTED_ACTOR");

// check the ledger reference
await expect(await nftVault.getTrustedLedger()).eql(ledger.address);
});

it("Does not own the NFT", async function() {
const { keyVault, locksmith,
notary, ledger, nftVault, nft,
owner, root, second, third } = await loadFixture(TrustTestFixtures.freshNFTVault);

// in this fixture, the owner does not own the nft
await expect(nftVault.connect(root)
.deposit(0, 2, nft.address))
.to.be.revertedWith("ERC721: transfer from incorrect owner");
});
});
});
37 changes: 37 additions & 0 deletions test/TrustTestUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,43 @@ TrustTestFixtures = (function() {
notary, ledger, vault, tokenVault, coin,
owner, root, second, third};
},

freshNFTVault: async function() {
const {keyVault, locksmith,
notary, ledger, vault, events,
owner, root, second, third} =
await TrustTestFixtures.fundedEtherVault();

const NFTVault = await ethers.getContractFactory("NFTVault");

const nftVault = await upgrades.deployProxy(NFTVault, [
locksmith.address, ledger.address
]);
await nftVault.deployed();

// Deploy shadow nft contract
const ShadowNFT = await ethers.getContractFactory("ShadowNFT");
const nft = await ShadowNFT.deploy("my_nft", "myNFT");

// mint shadow NFTs
await nft.connect(root).mint(1);
await nft.connect(second).mint(2);
await nft.connect(third).mint(3);

// approve minted NFTs from current owner Address to nftVault address
await nft.connect(root).approve(nftVault.address, 1);
await nft.connect(second).approve(nftVault.address, 2);
await nft.connect(third).approve(nftVault.address, 3);


await notary.connect(root).setTrustedLedgerRole(
0, 0, ledger.address, nftVault.address, true, stb('nft Vault'));

return {keyVault, locksmith, events,
notary, ledger, vault, nftVault, nft,
owner, root, second, third};
},

////////////////////////////////////////////////////////////
// fundedTokenVault
//
Expand Down
Loading