├── .gitignore ├── README.md ├── contracts └── RareRedirect.sol ├── hardhat.config.ts ├── package-lock.json ├── package.json ├── scripts └── deploy.ts ├── test └── rareRedirect.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | cache/ 3 | build/ 4 | typechain/ 5 | artifacts/ 6 | coverage* 7 | .env 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > There are far, far better things ahead than any we leave behind. - C.S. Lewis 2 | 3 | Thanks for participating in the RareRedirect experiment. 2021-2025. 4 | 5 | # RareRedirect 6 | 7 | RareRedirect is an experiment that allows users to decide where a website should redirect based on whether they are willing to pay more than the previous redirect purchaser. The contract is very simple. None of the code is audited, so proceed at your own risk. If you notice any bugs or have suggestions for improvements, please open an issue. I'm always open to feedback. 8 | 9 | Validation for URL correctness occurs on the frontend site. The current site to implement this experiment is [janetyellen.com](https://janetyellen.com). 10 | 11 | ## Development 12 | 13 | ### Install dependencies 14 | 15 | `npm i` 16 | 17 | ### Build Contracts and Generate Typechain Typeings 18 | 19 | `npm run compile` 20 | 21 | ### Run Contract Tests & Get Callstacks 22 | 23 | In one terminal run `npx hardhat node` 24 | 25 | Then in another run `npm run test` 26 | 27 | Notes: 28 | 29 | - The gas usage table may be incomplete (the gas report currently needs to run with the `--network localhost` flag; see below). 30 | 31 | ### Run Contract Tests and Generate Gas Usage Report 32 | 33 | In one terminal run `npx hardhat node` 34 | 35 | Then in another run `npm run test -- --network localhost` 36 | 37 | Notes: 38 | 39 | - When running with this `localhost` option, you get a gas report but may not get good callstacks 40 | - See [here](https://github.com/cgewecke/eth-gas-reporter#installation-and-config) for how to configure the gas usage report. 41 | 42 | ### Run Coverage Report for Tests 43 | 44 | `npm run coverage` 45 | 46 | Notes: 47 | 48 | - running a coverage report currently deletes artifacts, so after each coverage run you will then need to run `npx hardhat clean` followed by `npm run build` before re-running tests 49 | 50 | ### Deploy to Ethereum 51 | 52 | Create/modify network config in `hardhat.config.ts` and add API key and private key, then run: 53 | 54 | `npx hardhat run --network scripts/deploy.ts` 55 | 56 | ### Verify on Etherscan 57 | 58 | Using the [hardhat-etherscan plugin](https://hardhat.org/plugins/nomiclabs-hardhat-etherscan.html), add Etherscan API key to `hardhat.config.ts`, then run: 59 | 60 | `npx hardhat verify --network ` 61 | -------------------------------------------------------------------------------- /contracts/RareRedirect.sol: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * 4 | 5 | 6 | 7 | 888d888 8888b. 888d888 .d88b. 8 | 888P" "88b 888P" d8P Y8b 9 | 888 .d888888 888 88888888 10 | 888 888 888 888 Y8b. 11 | 888 "Y888888 888 "Y8888 12 | 13 | 14 | 15 | 888 d8b 888 16 | 888 Y8P 888 17 | 888 888 18 | 888d888 .d88b. .d88888 888 888d888 .d88b. .d8888b 888888 19 | 888P" d8P Y8b d88" 888 888 888P" d8P Y8b d88P" 888 20 | 888 88888888 888 888 888 888 88888888 888 888 21 | 888 Y8b. Y88b 888 888 888 Y8b. Y88b. Y88b. 22 | 888 "Y8888 "Y88888 888 888 "Y8888 "Y8888P "Y888 23 | 24 | 25 | 26 | This contract is unaudited. It's basically a ponzi. 27 | It's worse than a ponzi. It's definitely not "trustless". 28 | DNS is centralized. I'll change the URL if I deem it 29 | harmful/illegal/etc. No guarantees, no refunds. 30 | 31 | 32 | 33 | * 34 | * 35 | */ 36 | 37 | // SPDX-License-Identifier: MIT 38 | pragma solidity 0.8.5; 39 | 40 | import "@openzeppelin/contracts/access/Ownable.sol"; 41 | 42 | contract RareRedirect is Ownable { 43 | // minimum price required to change the `currentUrl` 44 | uint256 public priceFloor; 45 | // current URL where site will be redirected 46 | string currentUrl = ""; 47 | 48 | event redirectChange(string currentURL, uint256 priceFloor); 49 | 50 | /** 51 | * @notice returns the current redirect url 52 | * @return current url as set by highest price 53 | */ 54 | function getUrl() public view returns (string memory) { 55 | return currentUrl; 56 | } 57 | 58 | /** 59 | * @notice method use to set a new redirect url, most include payment 60 | * @dev URL validation occurs on frontend 61 | * @param newRedirectUrl the URL the user would like to use as a redirect 62 | * @return the newly set redirect URL 63 | */ 64 | function setUrlPayable(string memory newRedirectUrl) 65 | external 66 | payable 67 | returns (string memory) 68 | { 69 | require( 70 | msg.value > priceFloor, 71 | "Value must be greater than priceFloor" 72 | ); 73 | currentUrl = newRedirectUrl; 74 | priceFloor = msg.value; 75 | 76 | emit redirectChange(currentUrl, priceFloor); 77 | return currentUrl; 78 | } 79 | 80 | /** 81 | * @notice method for owner to set URL in case current URL is harmful 82 | * @dev URL validation occurs on frontend 83 | * @return the newly set redirect URL 84 | */ 85 | function setUrlForOwner(string memory ownerUrl) 86 | public 87 | onlyOwner 88 | returns (string memory) 89 | { 90 | currentUrl = ownerUrl; 91 | 92 | emit redirectChange(currentUrl, priceFloor); 93 | return currentUrl; 94 | } 95 | 96 | /** 97 | * @notice gets the current minimum price required to change the URL 98 | * @return price floor for URL change 99 | */ 100 | function getPriceFloor() public view returns (uint256) { 101 | return priceFloor; 102 | } 103 | 104 | /** 105 | * @notice withdraws funds to owner 106 | */ 107 | function withdrawAll() external onlyOwner { 108 | payable(owner()).transfer(address(this).balance); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import { config as dotEnvConfig } from "dotenv"; 2 | dotEnvConfig(); 3 | 4 | import { HardhatUserConfig } from "hardhat/types"; 5 | 6 | import "@nomiclabs/hardhat-waffle"; 7 | import "@typechain/hardhat"; 8 | import "@nomiclabs/hardhat-etherscan"; 9 | import "solidity-coverage"; 10 | 11 | const ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY; 12 | const ALCHEMY_API_KEY = process.env.ALCHEMY_API_KEY; 13 | const PRIVATE_KEY = process.env.PRIVATE_KEY; 14 | 15 | const config: HardhatUserConfig = { 16 | defaultNetwork: "hardhat", 17 | solidity: { 18 | compilers: [ 19 | { 20 | version: "0.8.5", 21 | settings: { 22 | optimizer: { 23 | enabled: true, 24 | runs: 999999, 25 | }, 26 | }, 27 | }, 28 | ], 29 | }, 30 | networks: { 31 | hardhat: {}, 32 | localhost: {}, 33 | ropsten: { 34 | url: `https://eth-ropsten.alchemyapi.io/v2/${ALCHEMY_API_KEY}`, 35 | accounts: [`0x${PRIVATE_KEY}`], 36 | }, 37 | coverage: { 38 | url: "http://127.0.0.1:8555", // Coverage launches its own ganache-cli client 39 | }, 40 | }, 41 | etherscan: { 42 | // Your API key for Etherscan 43 | // Obtain one at https://etherscan.io/ 44 | apiKey: ETHERSCAN_API_KEY, 45 | }, 46 | typechain: { 47 | target: "ethers-v5", 48 | }, 49 | }; 50 | 51 | export default config; 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rare-redirect", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "npm run clean && npm run compile", 8 | "clean": "npx hardhat clean", 9 | "compile": "npx hardhat compile", 10 | "test": "npx hardhat test", 11 | "coverage": "npm run build && npx hardhat coverage --temp artifacts --network coverage" 12 | }, 13 | "author": "nickbytes", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "@nomiclabs/hardhat-ethers": "^2.0.2", 17 | "@nomiclabs/hardhat-etherscan": "^2.1.2", 18 | "@nomiclabs/hardhat-waffle": "^2.0.1", 19 | "@openzeppelin/contracts": "^4.2.0", 20 | "@typechain/ethers-v5": "^7.0.1", 21 | "@typechain/hardhat": "^2.0.0", 22 | "@types/chai": "^4.2.18", 23 | "@types/chai-as-promised": "^7.1.1", 24 | "@types/mocha": "^8.2.2", 25 | "@types/node": "^15.0.3", 26 | "chai": "^4.3.4", 27 | "chai-as-promised": "^7.1.1", 28 | "dotenv": "^9.0.2", 29 | "ethereum-waffle": "^3.3.0", 30 | "ethers": "^5.1.4", 31 | "hardhat": "^2.2.1", 32 | "solc": "^0.8.5", 33 | "solidity-coverage": "^0.7.16", 34 | "ts-generator": "^0.1.1", 35 | "ts-node": "^9.1.1", 36 | "typechain": "^5.0.0", 37 | "typescript": "^4.2.4" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /scripts/deploy.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | 3 | async function main() { 4 | const factory = await ethers.getContractFactory("RareRedirect"); 5 | 6 | // If we had constructor arguments, they would be passed into deploy() 7 | let contract = await factory.deploy(); 8 | 9 | console.log( 10 | `The address the Contract WILL have once mined: ${contract.address}` 11 | ); 12 | 13 | console.log( 14 | `The transaction that was sent to the network to deploy the Contract: ${contract.deployTransaction.hash}` 15 | ); 16 | 17 | console.log( 18 | "The contract is NOT deployed yet; we must wait until it is mined..." 19 | ); 20 | await contract.deployed(); 21 | console.log("Mined!"); 22 | } 23 | 24 | main() 25 | .then(() => process.exit(0)) 26 | .catch((error) => { 27 | console.error(error); 28 | process.exit(1); 29 | }); 30 | -------------------------------------------------------------------------------- /test/rareRedirect.ts: -------------------------------------------------------------------------------- 1 | import { ethers, waffle } from "hardhat"; 2 | const { provider } = waffle; 3 | import chai from "chai"; 4 | import chaiAsPromised from "chai-as-promised"; 5 | import { RareRedirect__factory, RareRedirect } from "../typechain"; 6 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; 7 | 8 | chai.use(chaiAsPromised); 9 | const { expect } = chai; 10 | 11 | describe("RareRedirect", () => { 12 | let rareRedirect: RareRedirect; 13 | 14 | let owner: SignerWithAddress; 15 | let addr1: SignerWithAddress; 16 | let addr2: SignerWithAddress; 17 | let addrs: SignerWithAddress[]; 18 | 19 | beforeEach(async () => { 20 | [owner, addr1, addr2, ...addrs] = await ethers.getSigners(); 21 | 22 | const rareRedirectFactory = (await ethers.getContractFactory( 23 | "RareRedirect", 24 | owner 25 | )) as RareRedirect__factory; 26 | rareRedirect = await rareRedirectFactory.deploy(); 27 | await rareRedirect.deployed(); 28 | const initialUrl = await rareRedirect.getUrl(); 29 | 30 | expect(initialUrl).to.eq(""); 31 | expect(rareRedirect.address).to.properAddress; 32 | }); 33 | 34 | describe("set url", async () => { 35 | beforeEach(async () => { 36 | await rareRedirect 37 | .connect(owner) 38 | .functions.setUrlPayable("https://twitter.com/nickbytes", { 39 | from: owner.address, 40 | value: ethers.utils.parseEther("1.0"), 41 | }); 42 | }); 43 | 44 | it("should set url with 1.0eth floor", async () => { 45 | let url = await rareRedirect.getUrl(); 46 | expect(url).to.eq("https://twitter.com/nickbytes"); 47 | }); 48 | 49 | it("checks price at 1.0", async () => { 50 | const priceFloor = await rareRedirect.priceFloor(); 51 | expect(priceFloor).to.eq(ethers.utils.parseEther("1.0")); 52 | }); 53 | 54 | it("should revert if price is below floor", async () => { 55 | const failedBelowFloor = rareRedirect 56 | .connect(owner) 57 | .functions.setUrlPayable("https://twitter.com/VitalikButerin", { 58 | from: owner.address, 59 | value: ethers.utils.parseEther("0.5"), 60 | }); 61 | 62 | await expect(failedBelowFloor).to.be.reverted; 63 | }); 64 | 65 | it("should allow setUrl with over 1.0", async () => { 66 | await rareRedirect 67 | .connect(addr2) 68 | .functions.setUrlPayable("https://twitter.com/CryptoCobain", { 69 | from: addr2.address, 70 | value: ethers.utils.parseEther("2.5"), 71 | }); 72 | 73 | await expect(await rareRedirect.getUrl()).to.eq( 74 | "https://twitter.com/CryptoCobain" 75 | ); 76 | await expect(await rareRedirect.getPriceFloor()).to.eq( 77 | ethers.utils.parseEther("2.5") 78 | ); 79 | }); 80 | }); 81 | 82 | describe("owner set url", async () => { 83 | it("should set url when owner", async () => { 84 | await rareRedirect.setUrlForOwner("https://uniswap.org/"); 85 | let url = await rareRedirect.getUrl(); 86 | expect(url).to.eq("https://uniswap.org/"); 87 | }); 88 | 89 | it("should revert when not owner", async () => { 90 | const failed = rareRedirect 91 | .connect(addr1) 92 | .setUrlForOwner("https://www.binance.com/"); 93 | 94 | await expect(failed).to.be.reverted; 95 | }); 96 | }); 97 | 98 | describe("withdraw funds", async () => { 99 | beforeEach(async () => { 100 | await rareRedirect 101 | .connect(addr2) 102 | .functions.setUrlPayable("https://www.coincenter.org/", { 103 | from: addr2.address, 104 | value: ethers.utils.parseEther("4.20"), 105 | }); 106 | }); 107 | 108 | it("check that the contract has 4.20 ETH", async () => { 109 | const balance = await provider.getBalance(rareRedirect.address); 110 | await expect(balance).to.be.eq(ethers.utils.parseEther("4.20")); 111 | }); 112 | 113 | it("withdraws funds to owner", async () => { 114 | await rareRedirect.connect(owner).functions.withdrawAll(); 115 | const balance = await provider.getBalance(rareRedirect.address); 116 | await expect(balance).to.be.eq(ethers.utils.parseEther("0.0")); 117 | }); 118 | 119 | // Probably unnecessary to test OpenZeppelin Ownable here 120 | it("withdraws attempts from non-owners reverts", async () => { 121 | const failedWithdraw = rareRedirect 122 | .connect(addr2) 123 | .functions.withdrawAll(); 124 | await expect(failedWithdraw).to.be.revertedWith( 125 | "Ownable: caller is not the owner" 126 | ); 127 | }); 128 | }); 129 | }); 130 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "outDir": "dist", 8 | "resolveJsonModule": true 9 | }, 10 | "include": ["./scripts", "./test"], 11 | "files": ["./hardhat.config.ts"] 12 | } 13 | --------------------------------------------------------------------------------