feat: design ens verification flow (#193)

* wip: add base solidity functions to try ens validation

* chore: add hardhat fork setup with env variable

* wip: setup ens verification for FleekERC721

* wip: add ens namehash generation

* chore: gas report setting

* feat: add ens verification on setTokenENS function

* test: hardhat transfer ens owner to set new ens

* test: fix foundry tests to enable ens validation

* refactor: change back ens to calldata and split out a public function for namehash testing

* refactor: move FleekENS.sol to util folder

* test: add ens validation tests for not owner attempts

* chore: add mainnet api key env in ci

* chore: fix .env.example

* fix: remove extra unused file
This commit is contained in:
Felipe Mendes 2023-04-05 11:48:56 -03:00 committed by GitHub
parent 2225b301ff
commit 83cd36dfc3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 291 additions and 23 deletions

View File

@ -11,6 +11,8 @@ on:
jobs:
test-contracts:
runs-on: ubuntu-latest
env:
MAINNET_API_KEY: ${{ secrets.MAINNET_API_KEY }}
defaults:
run:

View File

@ -9,4 +9,13 @@ PRIVATE_KEY=
POLYSCAN_API=https://mumbai.polygonscan.com/
# The address of the deployed contract on the blockchain
CONTRACT_ADDRESS=
CONTRACT_ADDRESS=
# Alchemy or Infura API key to fork the mainnet on tests (e.g. https://eth-mainnet.g.alchemy.com/v2/your-api-key)
MAINNET_API_KEY=
# Enable gas report on hardhat tests
REPORT_GAS=true
# The CoinMarketCap API key to get the price of the token for gas report
COINMARKETCAP_KEY=

View File

@ -2,10 +2,11 @@
cache
artifacts
deployments/hardhat
gas-report
# Foundry
out
forge-cache
# OpenZeppelin
.openzeppelin/unknown-*.json
.openzeppelin/unknown-*.json

View File

@ -9,6 +9,7 @@ import "./FleekAccessControl.sol";
import "./FleekBilling.sol";
import "./FleekPausable.sol";
import "./FleekAccessPoints.sol";
import "./util/FleekENS.sol";
import "./util/FleekStrings.sol";
import "./IERCX.sol";
@ -104,7 +105,7 @@ contract FleekERC721 is
string memory name,
string memory description,
string memory externalURL,
string memory ENS,
string calldata ens,
string memory commitHash,
string memory gitRepository,
string memory logo,
@ -112,6 +113,7 @@ contract FleekERC721 is
bool accessPointAutoApproval,
address verifier
) public payable requirePayment(Billing.Mint) returns (uint256) {
FleekENS.requireENSOwner(ens);
uint256 tokenId = _appIds;
_mint(to, tokenId);
@ -121,7 +123,7 @@ contract FleekERC721 is
app.name = name;
app.description = description;
app.externalURL = externalURL;
app.ENS = ENS;
app.ENS = ens;
app.logo = logo;
app.color = color;
@ -134,7 +136,7 @@ contract FleekERC721 is
name,
description,
externalURL,
ENS,
ens,
commitHash,
gitRepository,
logo,
@ -274,8 +276,9 @@ contract FleekERC721 is
*/
function setTokenENS(
uint256 tokenId,
string memory _tokenENS
string calldata _tokenENS
) public virtual requireTokenRole(tokenId, TokenRoles.Controller) {
FleekENS.requireENSOwner(_tokenENS);
_requireMinted(tokenId);
_apps[tokenId].ENS = _tokenENS;
emit MetadataUpdate(tokenId, "ENS", _tokenENS, msg.sender);

View File

@ -0,0 +1,32 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {ENS} from "@ensdomains/ens-contracts/contracts/registry/ENS.sol";
import {Resolver} from "@ensdomains/ens-contracts/contracts/resolvers/Resolver.sol";
error MustBeENSOwner();
library FleekENS {
ENS internal constant _ens = ENS(0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e);
/**
* @dev Reverts if the sender is not the owner of the ENS node.
*/
function requireENSOwner(string calldata name) internal view {
if (_ens.owner(namehash(bytes(name), 0)) != msg.sender) revert MustBeENSOwner();
}
/**
* @dev Processes the name and returns the ENS node hash.
*/
function namehash(bytes calldata name, uint256 index) internal view returns (bytes32) {
for (uint256 i = index; i < name.length; i++) {
if (name[i] == ".") {
return keccak256(abi.encodePacked(namehash(name, i + 1), keccak256(name[index:i])));
}
}
return keccak256(abi.encodePacked(bytes32(0x0), keccak256(name[index:name.length])));
}
}

View File

@ -4,3 +4,8 @@ out = 'out'
libs = ['node_modules', 'lib']
test = 'test/foundry'
cache_path = 'forge-cache'
via_ir = true
[rpc_endpoints]
mainnet = "${MAINNET_API_KEY}"

View File

@ -18,6 +18,8 @@ const {
PRIVATE_KEY,
REPORT_GAS,
POLYGONSCAN_KEY,
MAINNET_API_KEY,
COINMARKETCAP_KEY,
} = process.env;
const config: HardhatUserConfig = {
@ -25,6 +27,12 @@ const config: HardhatUserConfig = {
networks: {
hardhat: {
chainId: 31337,
forking: MAINNET_API_KEY
? {
url: MAINNET_API_KEY,
blockNumber: 16876149,
}
: undefined,
},
mumbai: {
url: API_URL,
@ -35,9 +43,9 @@ const config: HardhatUserConfig = {
gasReporter: {
enabled: REPORT_GAS === 'true' || false,
currency: 'USD',
outputFile: 'gas-report.txt',
outputFile: 'gas-report',
noColors: true,
// coinmarketcap: process.env.COINMARKETCAP_API_KEY,
coinmarketcap: COINMARKETCAP_KEY,
},
contractSizer: {
runOnCompile: false,

View File

@ -4,8 +4,8 @@
"description": "",
"private": "false",
"scripts": {
"test": "hardhat test && forge test --via-ir",
"test:foundry": "forge test --via-ir -vvv",
"test": "yarn test:hardhat && yarn test:foundry",
"test:foundry": "forge test -vvv --fork-url mainnet --fork-block-number 16876149",
"test:hardhat": "hardhat test",
"format": "prettier --write \"./**/*.{js,json,sol,ts}\"",
"node:hardhat": "hardhat node",
@ -25,6 +25,8 @@
},
"homepage": "https://github.com/fleekxyz/non-fungible-apps#readme",
"devDependencies": {
"@ensdomains/ens-contracts": "^0.0.20",
"@ensdomains/eth-ens-namehash": "^2.0.15",
"@nomicfoundation/hardhat-chai-matchers": "^1.0.5",
"@nomicfoundation/hardhat-network-helpers": "^1.0.7",
"@nomicfoundation/hardhat-toolbox": "^2.0.0",

View File

@ -162,6 +162,7 @@ contract Test_FleekERC721_AccessControl is Test_FleekERC721_Base, Test_FleekERC7
function test_mint() public {
// Anyone can mint
transferENS(TestConstants.APP_ENS, anyAddress);
vm.startPrank(anyAddress);
mintDefault(address(99));
vm.stopPrank();
@ -201,7 +202,7 @@ contract Test_FleekERC721_AccessControl is Test_FleekERC721_Base, Test_FleekERC7
}
function test_setTokenENS() public {
string memory ens = "ens";
string memory ens = "ens.eth";
// ColletionOwner
vm.prank(collectionOwner);
@ -214,10 +215,12 @@ contract Test_FleekERC721_AccessControl is Test_FleekERC721_Base, Test_FleekERC7
CuT.setTokenENS(tokenId, ens);
// TokenOwner
transferENS(ens, tokenOwner);
vm.prank(tokenOwner);
CuT.setTokenENS(tokenId, ens);
// TokenController
transferENS(ens, tokenController);
vm.prank(tokenController);
CuT.setTokenENS(tokenId, ens);

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,32 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "./TestBase.sol";
import {Utils} from "./Utils.sol";
contract Test_FleekERC721_ENS is Test_FleekERC721_Base {
function expectRevertWithMustBeENSOwner() internal {
vm.expectRevert(MustBeENSOwner.selector);
}
function setUp() public {
baseSetUp();
}
function testFuzz_cannotMintIfNotENSOwner(address account) public {
vm.assume(deployer != account);
vm.prank(account);
expectRevertWithMustBeENSOwner();
mintDefault(account);
}
function testFuzz_cannotSetTokenENSIfNotENSOwner(address account) public {
vm.assume(deployer != account);
mintDefault(account);
vm.prank(account);
expectRevertWithMustBeENSOwner();
CuT.setTokenENS(0, TestConstants.APP_ENS);
}
}

View File

@ -18,7 +18,7 @@ contract Test_FleekERC721_GetToken is Test_FleekERC721_Base {
string memory name,
string memory description,
string memory externalURL,
string memory ENS,
string memory ens,
uint256 currentBuild,
string memory logo,
uint24 color
@ -28,7 +28,7 @@ contract Test_FleekERC721_GetToken is Test_FleekERC721_Base {
assertEq(externalURL, TestConstants.APP_EXTERNAL_URL);
assertEq(logo, TestConstants.LOGO_0);
assertEq(color, TestConstants.APP_COLOR);
assertEq(ENS, TestConstants.APP_ENS);
assertEq(ens, TestConstants.APP_ENS);
assertEq(currentBuild, 0);
}
@ -45,6 +45,7 @@ contract Test_FleekERC721_GetToken is Test_FleekERC721_Base {
CuT.setTokenName(tokenId, newAppName);
CuT.setTokenDescription(tokenId, newDescription);
CuT.setTokenExternalURL(tokenId, newExternalURL);
transferENS(newENS, deployer);
CuT.setTokenENS(tokenId, newENS);
CuT.setTokenBuild(tokenId, newCommitHash, newRepository);
CuT.setTokenLogoAndColor(tokenId, newLogo, newColor);
@ -53,7 +54,7 @@ contract Test_FleekERC721_GetToken is Test_FleekERC721_Base {
string memory name,
string memory description,
string memory externalURL,
string memory ENS,
string memory ens,
uint256 currentBuild,
string memory logo,
uint24 color
@ -63,7 +64,7 @@ contract Test_FleekERC721_GetToken is Test_FleekERC721_Base {
assertEq(externalURL, newExternalURL);
assertEq(logo, newLogo);
assertEq(color, newColor);
assertEq(ENS, newENS);
assertEq(ens, newENS);
assertEq(currentBuild, 1);
}

View File

@ -26,6 +26,8 @@ contract Test_FleekERC721_Mint is Test_FleekERC721_Base {
function test_mintTwoTokensForTwoAddresses() public {
uint256 firstMint = mintDefault(deployer);
transferENS("fleek.eth", deployer);
uint256 secondMint = CuT.mint(
address(12),
"Different App Name",
@ -45,6 +47,7 @@ contract Test_FleekERC721_Mint is Test_FleekERC721_Base {
}
function test_mintWithAutoApprovalAPsOn() public {
transferENS("fleek.eth", deployer);
uint256 mint = CuT.mint(
address(12),
"Different App Name",
@ -83,6 +86,7 @@ contract Test_FleekERC721_Mint is Test_FleekERC721_Base {
bool autoApprovalAp
) public {
vm.assume(to != address(0));
transferENS(ens, deployer);
uint256 tokenId = CuT.mint(
to,
appName,

View File

@ -5,6 +5,7 @@ pragma solidity ^0.8.17;
import "forge-std/Test.sol";
import "contracts/FleekERC721.sol";
import {TestConstants} from "./Constants.sol";
import {Utils} from "./Utils.sol";
abstract contract Test_FleekERC721_Assertions is Test {
function expectRevertWithTokenRole(uint256 tokenId, FleekAccessControl.TokenRoles role) public {
@ -47,6 +48,7 @@ abstract contract Test_FleekERC721_Assertions is Test {
abstract contract Test_FleekERC721_Base is Test, Test_FleekERC721_Assertions {
FleekERC721 internal CuT; // Contract Under Test
address internal deployer;
ENS internal constant _ens = ENS(0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e);
function deployUninitialized() internal returns (FleekERC721) {
FleekERC721 _contract = new FleekERC721();
@ -59,6 +61,15 @@ abstract contract Test_FleekERC721_Base is Test, Test_FleekERC721_Assertions {
CuT = deployUninitialized();
CuT.initialize("Test Contract", "FLKAPS", new uint256[](0));
deployer = address(this);
transferENS(TestConstants.APP_ENS, deployer);
}
function transferENS(string memory ens, address newOwner) public {
bytes32 node = Utils.namehash(ens);
address ensOwner = _ens.owner(node);
vm.deal(ensOwner, 100000000000);
vm.prank(ensOwner);
_ens.setOwner(node, newOwner);
}
function mintDefault(address to) internal returns (uint256) {

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,16 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import {FleekENS} from "contracts/util/FleekENS.sol";
library Utils {
/**
* @dev This function is copyed from `FleekENS.sol`.
* It changes the `internal` modifier to `public` allowing it
* to be used in tests applying memory values
*/
function namehash(string calldata name) public view returns (bytes32) {
return FleekENS.namehash(bytes(name), 0);
}
}

View File

@ -0,0 +1,56 @@
import { expect } from 'chai';
import { TestConstants, Fixtures, Errors, transferENSNode } from './helpers';
import { loadFixture } from '@nomicfoundation/hardhat-network-helpers';
const { MintParams } = TestConstants;
describe('FleekERC721.ENS', () => {
let fixture: Awaited<ReturnType<typeof Fixtures.default>>;
beforeEach(async () => {
fixture = await loadFixture(Fixtures.default);
});
it('should not allow mint if not ENS owner', async () => {
const { contract, owner } = fixture;
await expect(
contract.mint(
owner.address,
MintParams.name,
MintParams.description,
MintParams.externalUrl,
'app.eth',
MintParams.commitHash,
MintParams.gitRepository,
MintParams.logo,
MintParams.color,
MintParams.accessPointAutoApprovalSettings,
owner.address
)
).to.be.revertedWithCustomError(contract, Errors.MustBeENSOwner);
});
it('should not allow set ENS if not ENS owner', async () => {
const { contract, owner } = fixture;
await transferENSNode('app.eth', owner);
await contract.mint(
owner.address,
MintParams.name,
MintParams.description,
MintParams.externalUrl,
'app.eth',
MintParams.commitHash,
MintParams.gitRepository,
MintParams.logo,
MintParams.color,
MintParams.accessPointAutoApprovalSettings,
owner.address
);
await expect(
contract.setTokenENS(0, 'subdomain.app.eth')
).to.be.revertedWithCustomError(contract, Errors.MustBeENSOwner);
});
});

View File

@ -15,4 +15,5 @@ export const Errors = Object.freeze({
PausableIsSetTo: 'PausableIsSetTo',
ThereIsNoTokenMinted: 'ThereIsNoTokenMinted',
RequiredPayment: 'RequiredPayment',
MustBeENSOwner: 'MustBeENSOwner',
});

View File

@ -1,11 +1,14 @@
import { ethers, upgrades } from 'hardhat';
import { TestConstants } from './constants';
import { transferENSNode } from './utils';
export abstract class Fixtures {
static async default() {
// Contracts are deployed using the first signer/account by default
const [owner, otherAccount] = await ethers.getSigners();
await transferENSNode(TestConstants.MintParams.ens, owner);
const libraries = {
FleekSVG: (await (await ethers.getContractFactory('FleekSVG')).deploy())
.address,

View File

@ -1,3 +1,10 @@
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers';
import { ethers, upgrades } from 'hardhat';
import { TestConstants } from './constants';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const namehash = require('@ensdomains/eth-ens-namehash');
export const parseTokenURI = (tokenURI: string) => {
const tokenURIDecoded = Buffer.from(
tokenURI.replace('data:application/json;base64,', ''),
@ -6,3 +13,25 @@ export const parseTokenURI = (tokenURI: string) => {
return JSON.parse(tokenURIDecoded);
};
export const getENSNode = (name: string) => {
return namehash.hash(namehash.normalize(name));
};
export const transferENSNode = async (name: string, to: SignerWithAddress) => {
const ens = await ethers.getContractAt(
'ENS',
'0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e'
);
const ensNode = getENSNode(name);
const ensOwner = await ethers.getImpersonatedSigner(await ens.owner(ensNode));
await to.sendTransaction({
to: ensOwner.address,
value: ethers.utils.parseEther('1000'),
});
await ens.connect(ensOwner).setOwner(ensNode, to.address);
};

View File

@ -1,6 +1,6 @@
import { loadFixture } from '@nomicfoundation/hardhat-network-helpers';
import { expect } from 'chai';
import { TestConstants, Fixtures, Events } from './helpers';
import { TestConstants, Fixtures, Events, transferENSNode } from './helpers';
const {
Logos: { 1: Logo1 },
@ -25,9 +25,11 @@ describe('FleekERC721.UpdateProperties', () => {
it('should emit event for ens change', async () => {
const { contract, tokenId, owner } = fixture;
await expect(contract.setTokenENS(tokenId, 'app.eth'))
await transferENSNode('subdomain.app.eth', owner);
await expect(contract.setTokenENS(tokenId, 'subdomain.app.eth'))
.to.emit(contract, Events.MetadataUpdate.string)
.withArgs(tokenId, 'ENS', 'app.eth', owner.address);
.withArgs(tokenId, 'ENS', 'subdomain.app.eth', owner.address);
});
it('should emit event for name change', async () => {

View File

@ -14,6 +14,33 @@
dependencies:
"@jridgewell/trace-mapping" "0.3.9"
"@ensdomains/buffer@^0.1.1":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@ensdomains/buffer/-/buffer-0.1.1.tgz#6c275ba7e457e935405b67876f1f0d980c8baa63"
integrity sha512-92SfSiNS8XorgU7OUBHo/i1ZU7JV7iz/6bKuLPNVsMxV79/eI7fJR6jfJJc40zAHjs3ha+Xo965Idomlq3rqnw==
"@ensdomains/ens-contracts@^0.0.20":
version "0.0.20"
resolved "https://registry.yarnpkg.com/@ensdomains/ens-contracts/-/ens-contracts-0.0.20.tgz#346eac70d666a7864142287ce1759b0f44bd8a5e"
integrity sha512-lAHQBVj2WtgbchcrE8ZuFI6DFq+O33wkLAGqsO2gcnn0EUJb65OJIdTqUfvfULKGJjkB2pyHfS/RgMSIW6h1Pw==
dependencies:
"@ensdomains/buffer" "^0.1.1"
"@ensdomains/solsha1" "0.0.3"
"@openzeppelin/contracts" "^4.1.0"
dns-packet "^5.3.0"
"@ensdomains/eth-ens-namehash@^2.0.15":
version "2.0.15"
resolved "https://registry.yarnpkg.com/@ensdomains/eth-ens-namehash/-/eth-ens-namehash-2.0.15.tgz#5e5f2f24ba802aff8bc19edd822c9a11200cdf4a"
integrity sha512-JRDFP6+Hczb1E0/HhIg0PONgBYasfGfDheujmfxaZaAv/NAH4jE6Kf48WbqfRZdxt4IZI3jl3Ri7sZ1nP09lgw==
"@ensdomains/solsha1@0.0.3":
version "0.0.3"
resolved "https://registry.yarnpkg.com/@ensdomains/solsha1/-/solsha1-0.0.3.tgz#fd479da9d40aadb59ff4fb4ec50632e7d2275a83"
integrity sha512-uhuG5LzRt/UJC0Ux83cE2rCKwSleRePoYdQVcqPN1wyf3/ekMzT/KZUF9+v7/AG5w9jlMLCQkUM50vfjr0Yu9Q==
dependencies:
hash-test-vectors "^1.3.2"
"@ethereumjs/common@2.5.0":
version "2.5.0"
resolved "https://registry.yarnpkg.com/@ethereumjs/common/-/common-2.5.0.tgz#ec61551b31bef7a69d1dc634d8932468866a4268"
@ -398,6 +425,11 @@
"@jridgewell/resolve-uri" "^3.0.3"
"@jridgewell/sourcemap-codec" "^1.4.10"
"@leichtgewicht/ip-codec@^2.0.1":
version "2.0.4"
resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b"
integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==
"@metamask/eth-sig-util@^4.0.0":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@metamask/eth-sig-util/-/eth-sig-util-4.0.1.tgz#3ad61f6ea9ad73ba5b19db780d40d9aae5157088"
@ -700,6 +732,11 @@
resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.8.1.tgz#363f7dd08f25f8f77e16d374350c3d6b43340a7a"
integrity sha512-1wTv+20lNiC0R07jyIAbHU7TNHKRwGiTGRfiNnA8jOWjKT98g5OgLpYWOi40Vgpk8SPLA9EvfJAbAeIyVn+7Bw==
"@openzeppelin/contracts@^4.1.0":
version "4.8.2"
resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.8.2.tgz#d815ade0027b50beb9bcca67143c6bcc3e3923d6"
integrity sha512-kEUOgPQszC0fSYWpbh2kT94ltOJwj1qfT2DWo+zVttmGmf97JZ99LspePNaeeaLhCImaHVeBbjaQFZQn7+Zc5g==
"@openzeppelin/contracts@^4.7.3":
version "4.8.1"
resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.8.1.tgz#709cfc4bbb3ca9f4460d60101f15dac6b7a2d5e4"
@ -2155,6 +2192,13 @@ dir-glob@^3.0.1:
dependencies:
path-type "^4.0.0"
dns-packet@^5.3.0:
version "5.4.0"
resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.4.0.tgz#1f88477cf9f27e78a213fb6d118ae38e759a879b"
integrity sha512-EgqGeaBB8hLiHLZtp/IbaDQTL8pZ0+IvwzSHA6d7VyMDM+B9hgddEMa9xjK5oYnw0ci0JQ6g2XCD7/f6cafU6g==
dependencies:
"@leichtgewicht/ip-codec" "^2.0.1"
dom-walk@^0.1.0:
version "0.1.2"
resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.2.tgz#0c548bef048f4d1f2a97249002236060daa3fd84"
@ -3302,6 +3346,11 @@ hash-base@^3.0.0:
readable-stream "^3.6.0"
safe-buffer "^5.2.0"
hash-test-vectors@^1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/hash-test-vectors/-/hash-test-vectors-1.3.2.tgz#f050fde1aff46ec28dcf4f70e4e3238cd5000f4c"
integrity sha512-PKd/fitmsrlWGh3OpKbgNLE04ZQZsvs1ZkuLoQpeIKuwx+6CYVNdW6LaPIS1QAdZvV40+skk0w4YomKnViUnvQ==
hash.js@1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.3.tgz#340dedbe6290187151c1ea1d777a3448935df846"