├── .soliumignore ├── _config.yml ├── docs ├── images │ ├── buy.png │ └── sell.png ├── help.md ├── _config.yml ├── index.md ├── process.md ├── events.md ├── clients │ ├── www.md │ └── cli.md └── contract.md ├── .gitignore ├── migrations ├── 1_initial_migration.js └── 2_deploy_contracts.js ├── test ├── helpers │ └── assertJump.js ├── contracts │ ├── MaliciousReferrer.sol │ ├── MaliciousSeller.sol │ └── MockEnsRegistrar.sol └── DomainSale.js ├── truffle.js ├── mkdocs.yml ├── package.json ├── .soliumrc.json ├── contracts ├── Migrations.sol ├── AbstractENS.sol ├── ENS.sol └── DomainSale.sol ├── README.md ├── x.sol └── LICENSE /.soliumignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /docs/images/buy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wealdtech/domainsale/HEAD/docs/images/buy.png -------------------------------------------------------------------------------- /docs/images/sell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wealdtech/domainsale/HEAD/docs/images/sell.png -------------------------------------------------------------------------------- /docs/help.md: -------------------------------------------------------------------------------- 1 | # Help 2 | 3 | For help with the DomainSale contract please use the [DomainSale Gitter](https://gitter.im/wealdtech/domainsale) 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Vim 2 | *.sw? 3 | 4 | # Truffle 5 | build 6 | 7 | # Node 8 | node_modules 9 | 10 | # Deployment scripts 11 | deploy*.sh 12 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-tactile 2 | title: DomainSale 3 | description: A secondary market for the Ethereum Name Service 4 | show_downloads: false 5 | -------------------------------------------------------------------------------- /migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | var Migrations = artifacts.require("./Migrations.sol"); 2 | 3 | module.exports = function(deployer) { 4 | deployer.deploy(Migrations); 5 | }; 6 | -------------------------------------------------------------------------------- /test/helpers/assertJump.js: -------------------------------------------------------------------------------- 1 | module.exports = function(error) { 2 | assert.isAbove(error.message.search('invalid opcode'), -1, 'Invalid opcode error must be returned'); 3 | } 4 | -------------------------------------------------------------------------------- /truffle.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | networks: { 3 | development: { 4 | host: "localhost", 5 | port: 8545, 6 | network_id: "*" // Match any network id 7 | } 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # DomainSale Documentation 2 | 3 | This documentation provides information for people who want to provide [DomainSale](https://github.com/wealdtech/domainsale) functionality within their applications. 4 | 5 | -------------------------------------------------------------------------------- /test/contracts/MaliciousReferrer.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.11; 2 | 3 | 4 | // A referrer that acts like a normal DomainSale referer but throws 5 | // rather than accepting payment 6 | contract MaliciousReferrer { 7 | 8 | // Refuse to receive funds 9 | function () payable { 10 | revert(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: DomainSale 2 | pages: 3 | - Home: index.md 4 | - Process: process.md 5 | - Contract: contract.md 6 | - Events: events.md 7 | - Clients: 8 | - HTML and REST: clients/www.md 9 | - Command-line interface: clients/cli.md 10 | - Help: help.md 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "domainsale", 3 | "description": "Domain sale contracts", 4 | "version": "0.0.1", 5 | "devDependencies": { 6 | "mocha": "*" 7 | }, 8 | "dependencies": { 9 | "bignumber": "^1.1.0", 10 | "solidity-sha3": "^0.4.1", 11 | "wealdtech-solidity": "^0.9.2", 12 | "zeppelin-solidity": "^1.3.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.soliumrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "custom-rules-filename": null, 3 | "rules": { 4 | "imports-on-top": true, 5 | "variable-declarations": true, 6 | "array-declarations": true, 7 | "operator-whitespace": true, 8 | "lbrace": true, 9 | "mixedcase": true, 10 | "camelcase": true, 11 | "uppercase": true, 12 | "no-with": true, 13 | "no-empty-blocks": true, 14 | "no-unused-vars": true, 15 | "double-quotes": true, 16 | "blank-lines": true, 17 | "indentation": true, 18 | "whitespace": true, 19 | "deprecated-suicide": true, 20 | "pragma-on-top": true 21 | } 22 | } -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.4; 2 | 3 | contract Migrations { 4 | address public owner; 5 | uint public last_completed_migration; 6 | 7 | modifier restricted() { 8 | if (msg.sender == owner) _; 9 | } 10 | 11 | function Migrations() { 12 | owner = msg.sender; 13 | } 14 | 15 | function setCompleted(uint completed) restricted { 16 | last_completed_migration = completed; 17 | } 18 | 19 | function upgrade(address new_address) restricted { 20 | Migrations upgraded = Migrations(new_address); 21 | upgraded.setCompleted(last_completed_migration); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /docs/process.md: -------------------------------------------------------------------------------- 1 | # DomainSale Process 2 | 3 | ## Process when Selling a Domain 4 | 5 | The process for selling a domain is shown below. Transitions caused by the seller sending transactions are surrounded with square brackets, and transitions caused by other parties are surrounded with angled brackets. 6 | 7 | ![Sell process](images/sell.png) 8 | 9 | ## Process when Buying a Domain 10 | 11 | The process for buying a domain is shown below. Transitions caused by the buyer sending transactions are surrounded with square brackets, and transitions caused by other parties are surrounded with angled brackets. 12 | 13 | ![Buy process](images/buy.png) 14 | 15 | -------------------------------------------------------------------------------- /test/contracts/MaliciousSeller.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.11; 2 | 3 | 4 | import '../../contracts/DomainSale.sol'; 5 | 6 | 7 | // A seller that acts like a normal DomainSale seller but throws 8 | // rather than accepting payment 9 | contract MaliciousSeller { 10 | 11 | // Receive funds in constructor 12 | function MaliciousSeller() payable { 13 | } 14 | 15 | // Refuse to receive funds 16 | function () payable { 17 | revert(); 18 | } 19 | 20 | // Transfer an ENS domain 21 | function transfer(Registrar registrar, string name, address to) { 22 | registrar.transfer(keccak256(name), to); 23 | } 24 | 25 | // Offer a domain for sale 26 | function offer(DomainSale domainSale, string name, uint256 price, uint256 reserve, address referrer) { 27 | domainSale.offer(name, price, reserve, referrer); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /migrations/2_deploy_contracts.js: -------------------------------------------------------------------------------- 1 | const sha3 = require('solidity-sha3').default; 2 | 3 | const ENS = artifacts.require("./ENS.sol"); 4 | const DomainSale = artifacts.require("./DomainSale.sol"); 5 | 6 | // Addresses from 'testrpc -d' 7 | const ensOwner = '0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1'; // accounts[0] 8 | const registrarOwner = '0xffcf8fdee72ac11b5c542428b35eef5769c409f0'; // accounts[1] 9 | const domainSaleOwner = '0x22d491bde2303f2f43325b2108d26f1eaba1e32b'; // accounts[2] 10 | 11 | module.exports = function(deployer) { 12 | return deployer.deploy(ENS, {from: ensOwner}).then(() => { 13 | return ENS.deployed().then(ens => { 14 | return deployer.deploy(DomainSale, ens.address, {from: domainSaleOwner}).then(() => { 15 | return DomainSale.deployed(); 16 | }); 17 | }); 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /contracts/AbstractENS.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.0; 2 | 3 | contract AbstractENS { 4 | function owner(bytes32 node) constant returns(address); 5 | function resolver(bytes32 node) constant returns(address); 6 | function ttl(bytes32 node) constant returns(uint64); 7 | function setOwner(bytes32 node, address owner); 8 | function setSubnodeOwner(bytes32 node, bytes32 label, address owner); 9 | function setResolver(bytes32 node, address resolver); 10 | function setTTL(bytes32 node, uint64 ttl); 11 | 12 | // Logged when the owner of a node assigns a new owner to a subnode. 13 | event NewOwner(bytes32 indexed node, bytes32 indexed label, address owner); 14 | 15 | // Logged when the owner of a node transfers ownership to a new account. 16 | event Transfer(bytes32 indexed node, address owner); 17 | 18 | // Logged when the resolver for a node changes. 19 | event NewResolver(bytes32 indexed node, address resolver); 20 | 21 | // Logged when the TTL of a node changes 22 | event NewTTL(bytes32 indexed node, uint64 ttl); 23 | } 24 | -------------------------------------------------------------------------------- /docs/events.md: -------------------------------------------------------------------------------- 1 | # Contract Events 2 | 3 | The DomainSale smart contract emits a number of events to allow listenres to provide full information about the current state of domains for sale and under auction. The events emitted are as follows: 4 | 5 | ### Offer 6 | 7 | This event is emitted when an `offer` transaction is received. The event emits the following parameters: 8 | 9 | - `seller` the address selling the domain (this is indexed) 10 | - `name` the name of the domain, minus the `.eth` ending (so for example if the offer is on `mydomain.eth` this would be `mydomain`) 11 | - `price` the fixed price at which the domain can be purchased immediately (if this is `0` it means that this domain cannot be purchased immediately) 12 | - `reserve` the lowest acceptable bid at which the domain will enter auction (if this is `0` it means that this domain cannot be purchased at auction) 13 | 14 | ### Bid 15 | 16 | This event is emitted when a `bid` transaction is received. The event emits the following parameters: 17 | 18 | - `bidder` the address making the bid (this is indexed) 19 | - `name` the name of the domain, minus the `.eth` ending (so for example if the bid is on `mydomain.eth` this would be `mydomain`) 20 | - `bid` the bid that was made 21 | 22 | ### Cancel 23 | 24 | This event is emitted when a `cancel` transaction is received. The event emits the following parameters: 25 | 26 | - `seller` the address selling the domain (this is indexed) 27 | - `name` the name of the domain, minus the `.eth` ending (so for example if the cancellation is on `mydomain.eth` this would be `mydomain`) 28 | 29 | ### Transfer 30 | 31 | This event is emitted when a domain is transferred, after either a `buy` or a `finish` transaction is received. The event emits the following parameters: 32 | 33 | - `seller` the address of the domain seller (this is indexed) 34 | - `buyer` the address to of the domain buyer (this is indexed) 35 | - `name` the name of the domain, minus the `.eth` ending (so for example if the domain transferred is `mydomain.eth` this would be `mydomain`) 36 | - `value` the amount that the buyer paid to buy the domain 37 | 38 | -------------------------------------------------------------------------------- /docs/clients/www.md: -------------------------------------------------------------------------------- 1 | Information about the current state of sales within DomainSale is available from a web interface, providing both HTML and JSON. 2 | 3 | ## HTML Interface 4 | 5 | The [HTML interface](http://domainsale.wealdtech.com/) provides information about every name in the DomainSale system. It shows if the name is available for purchase or bidding, what the required prices are, and when any on-going auction is scheduled to close. 6 | 7 | ## REST API 8 | 9 | The REST API provides the same information as available from the web interface. The endpoints available for the API are as follows: 10 | 11 | ### [/sales/](http://domainsale.wealdtech.com/sales/) 12 | 13 | This provides all names currently in the DomainSale system. Query parameters are: 14 | 15 | * `query` a string used for substring matches. Defaults to '' 16 | * `limit` the maximum number of sales to return. Defaults to 20 17 | * `offset` the number from which to start returning sales. Defaults to 0 18 | 19 | The returned JSON will include the following elements: 20 | 21 | * `status`: 0 if the request was successful, otherwise non-0 22 | * `data`: an array of sale objects 23 | 24 | A sample output might be: 25 | 26 | ```` 27 | { 28 | "data": [ 29 | { 30 | "contract": "5Fb681680d5C0d6d0c848a9D4527FFb7DfB9151d", 31 | "domain": "academia", 32 | "seller": "388Ea662EF2c223eC0B047D41Bf3c0f362142ad5", 33 | "price": 4000000000000000000, 34 | "reserve": 700000000000000000, 35 | "deedvalue": 10000000000000000 36 | }, 37 | { 38 | "contract": "5Fb681680d5C0d6d0c848a9D4527FFb7DfB9151d", 39 | "domain": "bloomberg", 40 | "seller": "388Ea662EF2c223eC0B047D41Bf3c0f362142ad5", 41 | "reserve": 200000000000000000, 42 | "deedvalue": 10000000000000000 43 | } 44 | ], 45 | "status": 0 46 | } 47 | ```` 48 | 49 | ### [/meta/sales](http://domainsale.wealdtech.com/meta/sales) 50 | 51 | This provides metadata about the sales in the DomainSale system. 52 | 53 | The returned JSON will include the following elements: 54 | 55 | * `status`: 0 if the request was successful, otherwise non-0 56 | * `data`: the metadata for the sales 57 | 58 | A sample output might be: 59 | 60 | ```` 61 | { 62 | "data": { 63 | "total": 201 64 | }, 65 | "status": 0 66 | } 67 | ```` 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## DomainSale 2 | 3 | Sell ENS domains through a smart contract. 4 | 5 | [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/wealdtech/domainsale) 6 | 7 | ## What is DomainSale and how does it work? 8 | 9 | Please refer to the introductory article to find out about Domainsale. 10 | 11 | ## How can I use DomainSale? 12 | 13 | There are a number of websites and applications for both buyers and sellers. These include: 14 | 15 | - Command-line utility for manging domain sales. Useful for managing bulk buying or selling of domains. Available at http://www.wealdtech.com/domainsale.html 16 | 17 | ## How can I write my own app using DomainSale? 18 | 19 | Details of how to develop apps using DomainSale are available at http://domainsale.readthedocs.io/ 20 | 21 | ## FAQ 22 | 23 | ### Why do I need to hand the deed to the DomainSale contract before making it available for sale? 24 | 25 | This is a required for two reasons. First, to prove that you do indeed own the domain. Second, so that the domain can be handed to the winning bidder by the contract when the sale finishes. 26 | 27 | ### What happens to the funds held in the deed when a sale finishes? 28 | 29 | The funds are retained in the deed and transferred to the new owner. 30 | 31 | ### How do I withdraw my name from sale? 32 | 33 | As long as no bids have been received for the name you can withdraw it from sale by issuing a `cancel()` transaction. The deed will be returned to the address from which it was sent. 34 | 35 | ### How do I retain my name if it is already under auction? 36 | 37 | Once an auction has started the only way for you to retain a name is to become the winning bidder. Note that to do so you should bid with an address different to that which put the domain up for auction. 38 | 39 | ### Can I sell subdomains? 40 | 41 | No; DomainSale does not support the selling of subdomains. 42 | 43 | ### What happens if an invalid domain is auctioned? 44 | 45 | DomainSale itself is unaware that a domain might be invalid. However, if at any time during an auction the domain is invalidated in ENS the auction process will not allow the auction to finish. The bidder can send an `invalidate()` message to DomainSale and retrieve the funds they have bid. 46 | 47 | ### How can I obtain support for DomainSale? 48 | 49 | Support for DomainSale can be found in the [DomainSale Gittter](https://gitter.im/wealdtech/domainsale). 50 | 51 | -------------------------------------------------------------------------------- /contracts/ENS.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.0; 2 | 3 | import './AbstractENS.sol'; 4 | 5 | /** 6 | * The ENS registry contract. 7 | */ 8 | contract ENS is AbstractENS { 9 | struct Record { 10 | address owner; 11 | address resolver; 12 | uint64 ttl; 13 | } 14 | 15 | mapping(bytes32=>Record) records; 16 | 17 | // Permits modifications only by the owner of the specified node. 18 | modifier only_owner(bytes32 node) { 19 | if (records[node].owner != msg.sender) throw; 20 | _; 21 | } 22 | 23 | /** 24 | * Constructs a new ENS registrar. 25 | */ 26 | function ENS() { 27 | records[0].owner = msg.sender; 28 | } 29 | 30 | /** 31 | * Returns the address that owns the specified node. 32 | */ 33 | function owner(bytes32 node) constant returns (address) { 34 | return records[node].owner; 35 | } 36 | 37 | /** 38 | * Returns the address of the resolver for the specified node. 39 | */ 40 | function resolver(bytes32 node) constant returns (address) { 41 | return records[node].resolver; 42 | } 43 | 44 | /** 45 | * Returns the TTL of a node, and any records associated with it. 46 | */ 47 | function ttl(bytes32 node) constant returns (uint64) { 48 | return records[node].ttl; 49 | } 50 | 51 | /** 52 | * Transfers ownership of a node to a new address. May only be called by the current 53 | * owner of the node. 54 | * @param node The node to transfer ownership of. 55 | * @param owner The address of the new owner. 56 | */ 57 | function setOwner(bytes32 node, address owner) only_owner(node) { 58 | Transfer(node, owner); 59 | records[node].owner = owner; 60 | } 61 | 62 | /** 63 | * Transfers ownership of a subnode sha3(node, label) to a new address. May only be 64 | * called by the owner of the parent node. 65 | * @param node The parent node. 66 | * @param label The hash of the label specifying the subnode. 67 | * @param owner The address of the new owner. 68 | */ 69 | function setSubnodeOwner(bytes32 node, bytes32 label, address owner) only_owner(node) { 70 | var subnode = sha3(node, label); 71 | NewOwner(node, label, owner); 72 | records[subnode].owner = owner; 73 | } 74 | 75 | /** 76 | * Sets the resolver address for the specified node. 77 | * @param node The node to update. 78 | * @param resolver The address of the resolver. 79 | */ 80 | function setResolver(bytes32 node, address resolver) only_owner(node) { 81 | NewResolver(node, resolver); 82 | records[node].resolver = resolver; 83 | } 84 | 85 | /** 86 | * Sets the TTL for the specified node. 87 | * @param node The node to update. 88 | * @param ttl The TTL in seconds. 89 | */ 90 | function setTTL(bytes32 node, uint64 ttl) only_owner(node) { 91 | NewTTL(node, ttl); 92 | records[node].ttl = ttl; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /test/contracts/MockEnsRegistrar.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.11; 2 | 3 | 4 | contract AbstractENS { 5 | function setSubnodeOwner(bytes32 node, bytes32 hash, address owner); 6 | function setOwner(bytes32 node, address owner); 7 | function setResolver(bytes32 node, address resolver); 8 | function owner(bytes32 node) returns (address); 9 | } 10 | 11 | 12 | contract Deed { 13 | address public registrar; 14 | address constant BURN = 0xdead; 15 | uint public creationDate; 16 | address public owner; 17 | address public previousOwner; 18 | uint public value; 19 | event OwnerChanged(address newOwner); 20 | event DeedClosed(); 21 | bool active; 22 | 23 | modifier onlyRegistrar { 24 | require(msg.sender == registrar); 25 | _; 26 | } 27 | 28 | modifier onlyActive { 29 | require(active); 30 | _; 31 | } 32 | 33 | function Deed(address _owner) payable { 34 | owner = _owner; 35 | registrar = msg.sender; 36 | creationDate = now; 37 | active = true; 38 | value = msg.value; 39 | } 40 | 41 | function setOwner(address newOwner) onlyRegistrar { 42 | require(newOwner != 0); 43 | previousOwner = owner; // This allows contracts to check who sent them the ownership 44 | owner = newOwner; 45 | OwnerChanged(newOwner); 46 | } 47 | 48 | function setRegistrar(address newRegistrar) onlyRegistrar { 49 | registrar = newRegistrar; 50 | } 51 | 52 | function setBalance(uint newValue, bool throwOnFailure) onlyRegistrar onlyActive { 53 | // Check if it has enough balance to set the value 54 | require(value >= newValue); 55 | value = newValue; 56 | // Send the difference to the owner 57 | if (!owner.send(this.balance - newValue) && throwOnFailure) { 58 | revert(); 59 | } 60 | } 61 | 62 | /** 63 | * @dev Close a deed and refund a specified fraction of the bid value 64 | * 65 | * @param refundRatio The amount*1/1000 to refund 66 | */ 67 | function closeDeed(uint refundRatio) onlyRegistrar onlyActive { 68 | active = false; 69 | assert(BURN.send(((1000 - refundRatio) * this.balance)/1000)); 70 | DeedClosed(); 71 | destroyDeed(); 72 | } 73 | 74 | /** 75 | * @dev Close a deed and refund a specified fraction of the bid value 76 | */ 77 | function destroyDeed() { 78 | require(!active); 79 | 80 | // Instead of selfdestruct(owner), invoke owner fallback function to allow 81 | // owner to log an event if desired; but owner should also be aware that 82 | // its fallback function can also be invoked by setBalance 83 | if (owner.send(this.balance)) { 84 | selfdestruct(BURN); 85 | } 86 | } 87 | } 88 | 89 | 90 | // A mock ENS registrar that acts as FIFS but contains deeds 91 | contract MockEnsRegistrar { 92 | AbstractENS public ens; 93 | bytes32 public rootNode; 94 | 95 | mapping (bytes32 => Entry) _entries; 96 | 97 | enum Mode { Open, Auction, Owned, Forbidden, Reveal, NotYetAvailable } 98 | 99 | struct Entry { 100 | Deed deed; 101 | uint registrationDate; 102 | uint value; 103 | uint highestBid; 104 | } 105 | 106 | // Payable for easy funding 107 | function MockEnsRegistrar(AbstractENS _ens, bytes32 _rootNode) payable { 108 | ens = _ens; 109 | rootNode = _rootNode; 110 | } 111 | 112 | modifier onlyOwner(bytes32 hash) { 113 | require(msg.sender == _entries[hash].deed.owner()); 114 | _; 115 | } 116 | 117 | modifier onlyUnregistered(bytes32 hash) { 118 | require(_entries[hash].deed == Deed(0)); 119 | _; 120 | } 121 | 122 | function register(bytes32 hash) payable onlyUnregistered(hash) { 123 | _entries[hash].deed = (new Deed).value(msg.value)(msg.sender); 124 | _entries[hash].registrationDate = now; 125 | _entries[hash].value = msg.value; 126 | _entries[hash].highestBid = msg.value; 127 | ens.setSubnodeOwner(rootNode, hash, msg.sender); 128 | } 129 | 130 | function state(bytes32 hash) constant returns (Mode) { 131 | if (_entries[hash].registrationDate > 0) { 132 | return Mode.Owned; 133 | } else { 134 | return Mode.Open; 135 | } 136 | } 137 | 138 | function entries(bytes32 hash) constant returns (Mode, address, uint, uint, uint) { 139 | Entry storage h = _entries[hash]; 140 | return (state(hash), h.deed, h.registrationDate, h.value, h.highestBid); 141 | } 142 | 143 | function deed(bytes32 hash) constant returns (address) { 144 | return _entries[hash].deed; 145 | } 146 | 147 | function transfer(bytes32 hash, address newOwner) onlyOwner(hash) { 148 | require(newOwner != 0); 149 | 150 | _entries[hash].deed.setOwner(newOwner); 151 | ens.setSubnodeOwner(rootNode, hash, newOwner); 152 | } 153 | 154 | // This allows anyone to invalidate any entry. It's purely for testing 155 | // purposes and should never be seen in a live contract. 156 | function invalidate(bytes32 hash) { 157 | Entry storage h = _entries[hash]; 158 | _tryEraseSingleNode(hash); 159 | _entries[hash].deed.closeDeed(0); 160 | h.value = 0; 161 | h.highestBid = 0; 162 | h.deed = Deed(0); 163 | } 164 | 165 | function _tryEraseSingleNode(bytes32 label) internal { 166 | if (ens.owner(rootNode) == address(this)) { 167 | ens.setSubnodeOwner(rootNode, label, address(this)); 168 | var node = sha3(rootNode, label); 169 | ens.setResolver(node, 0); 170 | ens.setOwner(node, 0); 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /docs/contract.md: -------------------------------------------------------------------------------- 1 | # The DomainSale Contract 2 | 3 | This section is for developers wishing to build their own tools using the DomainSale smart contract. If you have a question about the DomainSale contract not answered here please direct it to the [DomainSale Gittter](https://gitter.im/wealdtech/domainsale). 4 | 5 | The address of the DomainSale contract can be found in ENS at 'domainsale.eth'. This domain also contains the ABI of the DomainSale contract. It is recommended that access to the contract is carried out via the name rather than the address, in case the address is updated for any reason. 6 | 7 | Note that prior to selling a domain the ownership of that domain's deed must be transferred to DomainSale. Transferring a domain to the DomainSale contract involves a call to the ENS registrar's' `transfer` function. The address to which the domain should be transferred is the address of the DomainSale contract, as described above. 8 | 9 | For more details about the ENS registrar please see the [ENS documentation](http://docs.ens.domains). 10 | 11 | ## Carrying out actions with the DomainSale Contract 12 | 13 | ### offer 14 | 15 | To offer a domain for sale by the DomainSale contract a call to DomainSale's `offer` function is required. The offer function has the following parameters: 16 | 17 | - `name` the name of the domain, minus the `.eth` ending (so for example if you were offering `mydomain.eth` this would be `mydomain`) 18 | - `price` the fixed price at which the domain can be purchased immediately (if you do not want a fixed price sale then set this to `0`) 19 | - `reserve` the lowest acceptable bid at which the domain will enter auction (if you do not want an auction sale then set this to `0`) 20 | - `referrer` the referrer for this sale. This address will receive 5% of the final sale price of the domain regardless of the method with which it is sold (unless a subsequent offer for the same domain is made with a different referrer). 21 | 22 | This function must be called from the account that put the name up for sale. 23 | 24 | Multiple `offer` function calls can be made to change the price if required, but note that this is only possible if the domain auction has yet to start. 25 | 26 | ### cancel 27 | 28 | To cancel a domain sale by the DomainSale contract and return the deed to the previous owner a call to DomainSale's `cancel` function is required. The cancel function has the following parameters: 29 | 30 | - `name` the name of the domain, minus the `.eth` ending (so for example if you were offering `mydomain.eth` this would be `mydomain`) 31 | 32 | This function must be called from the account that put the name up for sale. 33 | 34 | Note that cancelling a domain sale is not possible once a domain has received a valid `bid` or `buy`. 35 | 36 | ### buy 37 | 38 | To buy a domain offered for sale with a fixed price a call to DomainSale's `buy` function is required. The buy function has the following parameters: 39 | 40 | - `name` the name of the domain, minus the `.eth` ending (so for example if you were buying `mydomain.eth` this would be `mydomain`) 41 | - `referrer` the referrer for this purchase. This address will receive 5% of the purchase price of the domain 42 | 43 | The buy function transfers the domain's deed to the buyer and the funds to the seller. 44 | 45 | ### bid 46 | 47 | To bid for a domain offered for sale by auction a call to DomainSale's `bid` function is required. The bid function has the following parameters: 48 | 49 | - `name` the name of the domain, minus the `.eth` ending (so for example if you were bidding on `mydomain.eth` this would be `mydomain`) 50 | - `referrer` the referrer for this bid. This address will receive 5% of the purchase price of the domain if this is the winning bid 51 | 52 | ### finish 53 | 54 | To finish an auction for a domain offered for sale a call to DomainSale's `finish` function is required. The finish function has the following parameters: 55 | 56 | - `name` the name of the domain, minus the `.eth` ending (so for example if you were offering `mydomain.eth` this would be `mydomain`) 57 | 58 | The finish function transfers the domain's deed to the winning bidder and the funds to the seller. 59 | 60 | ### withdraw 61 | 62 | To withdraw any outstanding funds from losing bids a call to DomainSale's `withdraw` function is required. 63 | 64 | ## Obtaining information with the DomainSale Contract 65 | 66 | ### isBuyable 67 | 68 | To find out if a domain can be bought using fixed price purchase a call to DomainSale's `isBuyable` function is required. The isBuyable function has the following parameters: 69 | 70 | - `name` the name of the domain, minus the `.eth` ending (so for example if you were checking `mydomain.eth` this would be `mydomain`) 71 | 72 | This will return `true` if the domain can be bought using fixed price purchase; otherwise false. 73 | 74 | ### price 75 | 76 | To find out the purchase price for a domain a call to DomainSale's `price` function is required. The result of this call is only valid if `isBuyable` returns `true`. The price function has the following parameters: 77 | 78 | - `name` the name of the domain, minus the `.eth` ending (so for example if you were checking `mydomain.eth` this would be `mydomain`) 79 | 80 | This will return the value that must be paid to purchase the domain. 81 | 82 | ### isAuction 83 | 84 | To find out if a domain can be bought at auction a call to DomainSale's `isAuction` function is required. The isAuction function has the following parameters: 85 | 86 | - `name` the name of the domain, minus the `.eth` ending (so for example if you were checking `mydomain.eth` this would be `mydomain`) 87 | 88 | This will return `true` if the domain can be bought at auction; otherwise false. 89 | 90 | 91 | ### auctionStarted 92 | 93 | To find out when a domain's auction started a call to DomainSale's `auctionStarted` function is required. The result of this call is only valid if `isAuction` returns `true`. The auctionStarted function has the following parameters: 94 | 95 | - `name` the name of the domain, minus the `.eth` ending (so for example if you were checking `mydomain.eth` this would be `mydomain`) 96 | 97 | This will return a timestamp for when the auction started. If `0` then it means that this auction has not yet started. 98 | 99 | ### auctionEnds 100 | 101 | To find out when a domain's auction ends a call to DomainSale's `auctionEnds` function is required. The result of this call is only valid if `isAuction` returns `true`. The auctionEnds function has the following parameters: 102 | 103 | - `name` the name of the domain, minus the `.eth` ending (so for example if you were checking `mydomain.eth` this would be `mydomain`) 104 | 105 | This will return a timestamp for when the auction ends. If `0` it means that this auction has not yet started. 106 | 107 | ### minimumBid 108 | 109 | To find out the minimum bid for a domain's auction ends a call to DomainSale's `minimumBid` function is required. The result of this call is only valid if `isAuction` returns `true`. The minimumBid function has the following parameters: 110 | 111 | - `name` the name of the domain, minus the `.eth` ending (so for example if you were checking `mydomain.eth` this would be `mydomain`) 112 | 113 | This will return a value for the minimum acceptable bid. 114 | 115 | ### balance 116 | 117 | To find out the returnable balance for an address due to losing bids at auction a call to DomainSale's `balance` function is required. 118 | 119 | This will return a value for the balance that will be returned the next time a `buy`, `bid` or `withdraw` function is called. 120 | -------------------------------------------------------------------------------- /x.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.2; 2 | contract Deed { 3 | address public owner; 4 | address public previousOwner; 5 | } 6 | contract Registry { 7 | function owner(bytes32 _hash) public constant returns (address); 8 | } 9 | contract Registrar { 10 | function transfer(bytes32 _hash, address newOwner) public; 11 | function entries(bytes32 _hash) public constant returns (uint, Deed, uint, uint, uint); 12 | } 13 | contract Permissioned { 14 | mapping(address=>mapping(bytes32=>bool)) internal permissions; 15 | bytes32 internal constant PERM_SUPERUSER = keccak256("_superuser"); 16 | function Permissioned() public { 17 | permissions[msg.sender][PERM_SUPERUSER] = true; 18 | } 19 | modifier ifPermitted(address addr, bytes32 permission) { 20 | require(permissions[addr][permission] || permissions[addr][PERM_SUPERUSER]); 21 | _; 22 | } 23 | function isPermitted(address addr, bytes32 permission) public constant returns (bool) { 24 | return(permissions[addr][permission] || permissions[addr][PERM_SUPERUSER]); 25 | } 26 | function setPermission(address addr, bytes32 permission, bool allowed) public ifPermitted(msg.sender, PERM_SUPERUSER) { 27 | permissions[addr][permission] = allowed; 28 | } 29 | } 30 | contract RegistryRef { 31 | function owner(bytes32 node) public constant returns (address); 32 | } 33 | contract ReverseRegistrarRef { 34 | function setName(string name) public returns (bytes32 node); 35 | } 36 | contract ENSReverseRegister { 37 | function ENSReverseRegister(address registry, string name) public { 38 | if (registry != 0) { 39 | var reverseRegistrar = RegistryRef(registry).owner(0x91d1777781884d03a6757a803996e38de2a42967fb37eeaca72729271025a9e2); 40 | if (reverseRegistrar != 0) { 41 | ReverseRegistrarRef(reverseRegistrar).setName(name); 42 | } 43 | } 44 | } 45 | } 46 | contract Pausable is Permissioned { 47 | event Pause(); 48 | event Unpause(); 49 | bool public paused = false; 50 | bytes32 internal constant PERM_PAUSE = keccak256("_pausable"); 51 | modifier ifNotPaused() { 52 | require(!paused); 53 | _; 54 | } 55 | modifier ifPaused { 56 | require(paused); 57 | _; 58 | } 59 | function pause() public ifPermitted(msg.sender, PERM_PAUSE) ifNotPaused returns (bool) { 60 | paused = true; 61 | Pause(); 62 | return true; 63 | } 64 | function unpause() public ifPermitted(msg.sender, PERM_PAUSE) ifPaused returns (bool) { 65 | paused = false; 66 | Unpause(); 67 | return true; 68 | } 69 | } 70 | library SafeMath { 71 | function mul(uint256 a, uint256 b) internal constant returns (uint256) { 72 | uint256 c = a * b; 73 | assert(a == 0 || c / a == b); 74 | return c; 75 | } 76 | function div(uint256 a, uint256 b) internal constant returns (uint256) { 77 | uint256 c = a / b; 78 | return c; 79 | } 80 | function sub(uint256 a, uint256 b) internal constant returns (uint256) { 81 | assert(b <= a); 82 | return a - b; 83 | } 84 | function add(uint256 a, uint256 b) internal constant returns (uint256) { 85 | uint256 c = a + b; 86 | assert(c >= a); 87 | return c; 88 | } 89 | } 90 | contract DomainSale is ENSReverseRegister, Pausable { 91 | using SafeMath for uint256; 92 | Registrar public registrar; 93 | mapping (string => Sale) private sales; 94 | mapping (address => uint256) private balances; 95 | uint256 private constant AUCTION_DURATION = 24 hours; 96 | uint256 private constant HIGH_BID_KICKIN = 7 days; 97 | uint256 private constant NORMAL_BID_INCREASE_PERCENTAGE = 10; 98 | uint256 private constant HIGH_BID_INCREASE_PERCENTAGE = 50; 99 | uint256 private constant SELLER_SALE_PERCENTAGE = 90; 100 | uint256 private constant START_REFERRER_SALE_PERCENTAGE = 5; 101 | uint256 private constant BID_REFERRER_SALE_PERCENTAGE = 5; 102 | string private constant CONTRACT_ENS = "domainsale.eth"; 103 | bytes32 private constant NAMEHASH_ETH = 0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae; 104 | struct Sale { 105 | uint256 price; 106 | uint256 reserve; 107 | uint256 lastBid; 108 | address lastBidder; 109 | uint256 auctionStarted; 110 | uint256 auctionEnds; 111 | address startReferrer; 112 | address bidReferrer; 113 | } 114 | event Offer(address indexed seller, string name, uint256 price, uint256 reserve); 115 | event Bid(address indexed bidder, string name, uint256 bid); 116 | event Transfer(address indexed seller, address indexed buyer, string name, uint256 value); 117 | event Cancel(string name); 118 | event Withdraw(address indexed recipient, uint256 amount); 119 | modifier onlyNameSeller(string _name) { 120 | Deed deed; 121 | (,deed,,,) = registrar.entries(keccak256(_name)); 122 | require(deed.owner() == address(this)); 123 | require(deed.previousOwner() == msg.sender); 124 | _; 125 | } 126 | modifier deedValid(string _name) { 127 | address deed; 128 | (,deed,,,) = registrar.entries(keccak256(_name)); 129 | require(deed != 0); 130 | _; 131 | } 132 | modifier auctionNotStarted(string _name) { 133 | require(sales[_name].auctionStarted == 0); 134 | _; 135 | } 136 | modifier canBid(string _name) { 137 | require(sales[_name].reserve != 0); 138 | _; 139 | } 140 | modifier canBuy(string _name) { 141 | require(sales[_name].price != 0); 142 | _; 143 | } 144 | function DomainSale(address _registry) public ENSReverseRegister(_registry, CONTRACT_ENS) { 145 | registrar = Registrar(Registry(_registry).owner(NAMEHASH_ETH)); 146 | } 147 | function sale(string _name) public constant returns (uint256, uint256, uint256, address, uint256, uint256) { 148 | Sale storage s = sales[_name]; 149 | return (s.price, s.reserve, s.lastBid, s.lastBidder, s.auctionStarted, s.auctionEnds); 150 | } 151 | function isAuction(string _name) public constant returns (bool) { 152 | return sales[_name].reserve != 0; 153 | } 154 | function isBuyable(string _name) public constant returns (bool) { 155 | return sales[_name].price != 0 && sales[_name].auctionStarted == 0; 156 | } 157 | function auctionStarted(string _name) public constant returns (bool) { 158 | return sales[_name].lastBid != 0; 159 | } 160 | function auctionEnds(string _name) public constant returns (uint256) { 161 | return sales[_name].auctionEnds; 162 | } 163 | function minimumBid(string _name) public constant returns (uint256) { 164 | Sale storage s = sales[_name]; 165 | if (s.auctionStarted == 0) { 166 | return s.reserve; 167 | } else if (s.auctionStarted.add(HIGH_BID_KICKIN) > now) { 168 | return s.lastBid.add(s.lastBid.mul(NORMAL_BID_INCREASE_PERCENTAGE).div(100)); 169 | } else { 170 | return s.lastBid.add(s.lastBid.mul(HIGH_BID_INCREASE_PERCENTAGE).div(100)); 171 | } 172 | } 173 | function price(string _name) public constant returns (uint256) { 174 | return sales[_name].price; 175 | } 176 | function balance(address addr) public constant returns (uint256) { 177 | return balances[addr]; 178 | } 179 | function offer(string _name, uint256 _price, uint256 reserve, address referrer) onlyNameSeller(_name) auctionNotStarted(_name) deedValid(_name) ifNotPaused public { 180 | require(_price == 0 || _price > reserve); 181 | require(_price != 0 || reserve != 0); 182 | Sale storage s = sales[_name]; 183 | s.reserve = reserve; 184 | s.price = _price; 185 | s.startReferrer = referrer; 186 | Offer(msg.sender, _name, _price, reserve); 187 | } 188 | function cancel(string _name) onlyNameSeller(_name) auctionNotStarted(_name) deedValid(_name) ifNotPaused public { 189 | delete sales[_name]; 190 | registrar.transfer(keccak256(_name), msg.sender); 191 | Cancel(_name); 192 | } 193 | function buy(string _name, address bidReferrer) canBuy(_name) deedValid(_name) ifNotPaused public payable { 194 | Sale storage s = sales[_name]; 195 | require(msg.value >= s.price); 196 | require(s.auctionStarted == 0); 197 | Deed deed; 198 | (,deed,,,) = registrar.entries(keccak256(_name)); 199 | address previousOwner = deed.previousOwner(); 200 | registrar.transfer(keccak256(_name), msg.sender); 201 | Transfer(previousOwner, msg.sender, _name, msg.value); 202 | distributeFunds(msg.value, previousOwner, s.startReferrer, bidReferrer); 203 | delete sales[_name]; 204 | withdraw(); 205 | } 206 | function bid(string _name, address bidReferrer) canBid(_name) deedValid(_name) ifNotPaused public payable { 207 | require(msg.value >= minimumBid(_name)); 208 | Sale storage s = sales[_name]; 209 | require(s.auctionStarted == 0 || now < s.auctionEnds); 210 | if (s.auctionStarted == 0) { 211 | s.auctionStarted = now; 212 | } else { 213 | balances[s.lastBidder] = balances[s.lastBidder].add(s.lastBid); 214 | } 215 | s.lastBidder = msg.sender; 216 | s.lastBid = msg.value; 217 | s.auctionEnds = now.add(AUCTION_DURATION); 218 | s.bidReferrer = bidReferrer; 219 | Bid(msg.sender, _name, msg.value); 220 | withdraw(); 221 | } 222 | function finish(string _name) deedValid(_name) ifNotPaused public { 223 | Sale storage s = sales[_name]; 224 | require(now > s.auctionEnds); 225 | Deed deed; 226 | (,deed,,,) = registrar.entries(keccak256(_name)); 227 | address previousOwner = deed.previousOwner(); 228 | registrar.transfer(keccak256(_name), s.lastBidder); 229 | Transfer(previousOwner, s.lastBidder, _name, s.lastBid); 230 | distributeFunds(s.lastBid, previousOwner, s.startReferrer, s.bidReferrer); 231 | delete sales[_name]; 232 | withdraw(); 233 | } 234 | function withdraw() ifNotPaused public { 235 | uint256 amount = balances[msg.sender]; 236 | if (amount > 0) { 237 | balances[msg.sender] = 0; 238 | msg.sender.transfer(amount); 239 | Withdraw(msg.sender, amount); 240 | } 241 | } 242 | function invalidate(string _name) ifNotPaused public { 243 | address deed; 244 | (,deed,,,) = registrar.entries(keccak256(_name)); 245 | require(deed == 0); 246 | Sale storage s = sales[_name]; 247 | balances[s.lastBidder] = balances[s.lastBidder].add(s.lastBid); 248 | delete sales[_name]; 249 | Cancel(_name); 250 | withdraw(); 251 | } 252 | function distributeFunds(uint256 amount, address seller, address startReferrer, address bidReferrer) internal { 253 | uint256 startReferrerFunds = amount.mul(START_REFERRER_SALE_PERCENTAGE).div(100); 254 | balances[startReferrer] = balances[startReferrer].add(startReferrerFunds); 255 | uint256 bidReferrerFunds = amount.mul(BID_REFERRER_SALE_PERCENTAGE).div(100); 256 | balances[bidReferrer] = balances[bidReferrer].add(bidReferrerFunds); 257 | uint256 sellerFunds = amount.sub(startReferrerFunds).sub(bidReferrerFunds); 258 | balances[seller] = balances[seller].add(sellerFunds); 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /docs/clients/cli.md: -------------------------------------------------------------------------------- 1 | The DomainSale command-line interface (CLI) provides access to the DomainSale process for both buyers and sellers. It is highly useful in automated or bulk environments, and allows scripting and other advanced integration with DomainSale. 2 | 3 | Before using the CLI, or any other DomainSale tool, please read the [introductory article](https://medium.com/@jgm.orinoco/domainsale-an-on-chain-secondary-ens-market-b3330f6e5dda) to ensure familiarity with the process for both buying and selling domains. 4 | 5 | # Installation and Setup 6 | 7 | DomainSale uses geth's keystores to hold account information. If you do not have geth installed on your computer then you should install it following the [official instructions](https://github.com/ethereum/go-ethereum/wiki/Building-Ethereum). 8 | 9 | To confirm that geth is configured correctly run `geth account list` in a terminal. You should see your addresses displayed, amongst other information. If you need to import addresses in to geth please [follow the official instructions](https://github.com/ethereum/go-ethereum/wiki/Managing-your-accounts). 10 | 11 | Operating system-specific installation instructions are provided below. 12 | 13 | ## Windows 14 | 15 | Download the relevant binary for your system architecture below. If you are not sure then then download the x86 binary. 16 | 17 | * [Windows AMD64 (64-bit)](http://www.wealdtech.com/domainsale/windows-amd64/domainsale.exe) 18 | * [Windows x86 (32-bit)](http://www.wealdtech.com/domainsale/windows-386/domainsale.exe) 19 | 20 | The binary is standalone and does not require installation, however it can be moved to `C:\Windows\System32` to make it accessible to all users. 21 | 22 | ## Linux 23 | 24 | Download the relevant binary for your system architecture below. If you are not sure then typing `uname -m` should provide you with an answer. 25 | 26 | * [Linux AMD64 (64-bit)](http://www.wealdtech.com/domainsale/linux-amd64/domainsale) 27 | * [Linux x86 (32-bit)](http://www.wealdtech.com/domainsale/linux-386/domainsale) 28 | * [Linux ARM5 (32-bit)](http://www.wealdtech.com/domainsale/linux-arm-5/domainsale) 29 | * [Linux ARM6 (32-bit)](http://www.wealdtech.com/domainsale/linux-arm-6/domainsale) 30 | * [Linux ARM7 (32-bit)](http://www.wealdtech.com/domainsale/linux-arm-7/domainsale) 31 | * [Linux ARM64 (64-bit)](http://www.wealdtech.com/domainsale/linux-arm64/domainsale) 32 | 33 | Once the binary has been downloaded it needs to be made executable with the command `chmod 755 domainsale`. The binary is standalone and does not require installation, however it can be moved to `/usr/bin` to make it accessible to all users. 34 | 35 | ## OSX 36 | 37 | Download the relevant binary for your system architecture below. If you are not sure then typing `uname -m` should provide you with an answer. 38 | 39 | * [OSX AMD64 (64-bit)](http://www.wealdtech.com/domainsale/osx-amd64/domainsale) 40 | * [OSX x86 (32-bit)](http://www.wealdtech.com/domainsale/osx-386/domainsale) 41 | 42 | Once the binary has been downloaded it needs to be made executable with the command `chmod 755 domainsale`. The binary is standalone and does not require installation, however it can be moved to `/usr/bin` to make it accessible to all users. 43 | 44 | 45 | # Commands 46 | 47 | DomainSale is configured as a single binary with sub-commands. Details on each of these commands is listed below. In addition, help can be obtained for any command by entereing the command with the `--help` option. 48 | 49 | There are a number of options that are available for all commands. These are: 50 | 51 | * `quiet` do not display any output, and provide an exit status of `0` or `1` to indicate success or failure. Details of exactly what constitutes success is provided in the help for each individual command 52 | * `connection` the connection to the geth instance that will be used to obtain DomainSale information and send transactions. This defaults to a remote server, which allows the CLI to be used from anywhere with a network connection. If you would prefer to use a different geth instance then you can change this to point to its HTTP port or IPC connection 53 | * `log` log information about transactions generated by the CLI to a log file. By default this will create a file `domainsale.log` in your home directory 54 | 55 | Any commands that send transactions have the following additional options: 56 | 57 | * `passphrase` the passphrase to unlock the account being used for the command. This is required to sign any transaction sent to the network, which is any action around buying or selling domains 58 | * `gasprice` the price of gas for the transaction. This defaults to a relatively low value of 4GWei. If you are in a hurry with your transaction you might need to increase this to 20GWei or even higher. An overview of the expected transaction wait time for differing gas prices can be seen at http://ethgasstation.info/ 59 | * `nonce` the nonce for the transaction. Sending multiple commands in quick succession (for example, offering or bidding on multiple domains as part of a script) require nonces to avoid the issue where one command might attempt to override another in the chain. Sample scripts using nonces are shown in the examples section at the end of this document 60 | 61 | ## General 62 | 63 | The DomainSale process for buyers consists of a number of possibilities depending on if the buyer wants to purchase a domain outright or to bid for a domain. 64 | 65 | ### search 66 | 67 | Search for domains that match a given substring. An example of this is: 68 | 69 | ```text 70 | domainsale search myd 71 | ``` 72 | 73 | Breaking this down: 74 | 75 | * `search` is the command to be carried out 76 | * `myd` is the string for which any matching domain sale is returned 77 | 78 | If domains are found this command will return info on each of them, for example: 79 | 80 | ``` 81 | mydomain61 auction closed at Sat Sep 23 06:40:37 with a winning bid of 0.2 Ether 82 | mydomain62 is under auction and closes at Sun Sep 24 08:18:04. It can be bid on for 0.44 Ether 83 | mydomain76 can be purchased for 7 Ether or auctioned with a starting bid of 0.18 Ether 84 | mydomain78 can be purchased for 8 Ether 85 | mydomain86 can be auctioned with a starting bid of 0.16 Ether 86 | ``` 87 | 88 | ### info 89 | 90 | Obtaining information about a sale is accomplished using the `info` command. An example of this is: 91 | 92 | ```text 93 | domainsale info mydomain.eth 94 | ``` 95 | 96 | Breaking this down: 97 | 98 | * `info` is the command to be carried out 99 | * `mydomain.eth` is the domain for which information is being sought 100 | 101 | If the domain is not for sale this command will return 102 | 103 | ```text 104 | mydomain.eth is not for sale 105 | ``` 106 | 107 | If the domain is for sale this command will return (for example) 108 | 109 | ```text 110 | mydomain.eth can be bought for 9 Ether 111 | Auction will start on bid of at least 0.7 Ether 112 | ``` 113 | 114 | If the domain is undergoing auction this command will return (for example) 115 | 116 | ```text 117 | Auction started at 2017-09-22 07:40:37 +0100 BST 118 | Winning bid is 0.2 Ether 119 | Next bid must be at least 0.22 Ether 120 | Auction will end if no further bids received by 2017-09-23 07:40:37 +0100 BST 121 | ``` 122 | 123 | ## Buying domains 124 | 125 | ### buy 126 | 127 | Purchasing a domain outright is accomplished using the `buy` command. An example of this is: 128 | 129 | ```text 130 | domainsale buy --price="4 ether" --address=0xce4a68eafa7eda08e16419a14c146e1277fdebb5 --passphrase=my_secret_phrase mydomain.eth 131 | ``` 132 | 133 | Breaking this down: 134 | 135 | * `price` is the amount of ether used to buy the domain 136 | * `address` is the address of the account purchasing the domain 137 | * `passphrase` is the passphrase to the account purchasing the domain 138 | * `mydomain.eth` is the domain being purchased 139 | 140 | If this command completes successfully it will present the transaction ID for the purchase of the domain. If it fails then it will present details about why it failed. 141 | 142 | ## Bidding for domains 143 | 144 | Bidding for a domain outright is accomplished using the `bid` command. An example of this is: 145 | 146 | ```text 147 | domainsale bid --bid="0.1 ether" --address=0xce4a68eafa7eda08e16419a14c146e1277fdebb5 --passphrase=my_secret_phrase mydomain.eth 148 | ``` 149 | 150 | Breaking this down: 151 | 152 | * `bid` is the amount of ether used to bid for the domain 153 | * `address` is the address of the account bidding for the domain 154 | * `passphrase` is the passphrase to the account bidding for the domain 155 | * `mydomain.eth` is the domain being bid upon 156 | 157 | If this command completes successfully it will present the transaction ID for the bid for the domain. If it fails then it will present details about why it failed. 158 | 159 | ## Winning and losing auctions 160 | 161 | ### finish 162 | 163 | After a domain auction auction has finished either the buyer or seller can finish the auction to exchange the domain for the winning bid. An example of this is: 164 | 165 | ```text 166 | domainsale finish --passphrase=my_secret_phrase mydomain.eth 167 | ``` 168 | 169 | Breaking this down: 170 | 171 | * `passphrase` is the passphrase to the account that bought or sold the domain 172 | * `mydomain.eth` is the domain for which the auction has finished 173 | 174 | If this command completes successfully it will present the transaction ID for the completion of the auction. If it fails then it will present details about why it failed. 175 | 176 | ## Checking and obtaining funds 177 | 178 | ### balance 179 | 180 | If a user bid as part of an auction but failed to win their funds will be held pending withdrawal. This is also true of all funds received by sellers of domains. This command lists any funds that are owed to the given address. An example of this is: 181 | 182 | ```text 183 | domainsale balance 0xce4a68eafa7eda08e16419a14c146e1277fdebb5 184 | ``` 185 | 186 | Breaking this down: 187 | 188 | * `0xce4a68eafa7eda08e16419a14c146e1277fdebb5` is the address of the account that is being checked for funds 189 | 190 | If this command completes successfully it will state the amount of funds pending withdrawal. If it fails then it will present details about why it failed. 191 | 192 | ### withdraw 193 | 194 | If a user bid as part of an auction but failed to win their funds will be held pending withdrawal. This is also true of all funds received by sellers of domains. This command withdraws any funds that are owed to the given address. An example of this is: 195 | 196 | ```text 197 | domainsale withdraw --passphrase=my_secret_phrase 0xce4a68eafa7eda08e16419a14c146e1277fdebb5 198 | ``` 199 | 200 | Breaking this down: 201 | 202 | * `passphrase` is the passphrase to the account that has funds to withdraw 203 | * `0xce4a68eafa7eda08e16419a14c146e1277fdebb5` is the address of the account that has funds to withdraw 204 | 205 | If this command completes successfully it will present the transaction ID for the withdrawal of the funds. If it fails then it will present details about why it failed. 206 | 207 | ## Selling domains 208 | 209 | ### transfer 210 | 211 | Prior to selling a domain on DomainSale the domain must be transferred to DomainSale's control. An example of this is: 212 | 213 | ```text 214 | domainsale transfer --passphrase=my_secret_phrase mydomain.eth 215 | ``` 216 | 217 | Breaking this down: 218 | 219 | * `passphrase` is the passphrase to the account that currently owns mydomain.eth 220 | * `mydomain.eth` is the domain being transferred 221 | 222 | If this command completes successfully it will present the transaction ID for the transfer of the domain. If it fails then it will present details about why it failed. 223 | 224 | ### offer 225 | 226 | A seller can offer a transferred domain for either or both of sale or auction. An example of this is: 227 | 228 | ```text 229 | domainsale offer --price="10 ether" --reserve="1 ether" --passphrase=my_secret_phrase mydomain.eth 230 | ``` 231 | 232 | Breaking this down: 233 | 234 | * `price` is the price at which the domain can be purchased directly; leave this out if not required 235 | * `reserve` is the price at which the domain can be auctioned; leave this out if not required 236 | * `passphrase` is the passphrase to the account that transferred mydomain.eth to DomainSale 237 | * `mydomain.eth` is the domain being offered 238 | 239 | If this command completes successfully it will present the transaction ID for the offer of the domain. If it fails then it will present details about why it failed. 240 | 241 | ### cancel 242 | 243 | A seller can cancel an offered domain if it has not been purchased and is not under auction. An example of this is: 244 | 245 | ```text 246 | domainsale cancel --passphrase=my_secret_phrase mydomain.eth 247 | ``` 248 | 249 | Breaking this down: 250 | 251 | * `passphrase` is the passphrase to the account that transferred mydomain.eth to DomainSale 252 | * `mydomain.eth` is the domain being cancelled 253 | 254 | If this command completes successfully it will present the transaction ID for the cancellation of the domain sale. If it fails then it will present details about why it failed. 255 | 256 | # Examples 257 | 258 | ## Bulk operations 259 | 260 | A common requirement is to run bulk operations. A seller might want to list many domains, or a buyer might wish to bid on many domains. This can be accompished using the domainsale CLI and suitable scripting, for example using the script below: 261 | 262 | ```bash 263 | # The address of the owning account of the names to be transferred 264 | DOMAINS_OWNER=0x18a745ca319caf07a34049092c856f6bf8b367f3 265 | # The current nonce 266 | NONCE=`domainsale nonce ${DOMAINS_OWNER}` 267 | 268 | for DOMAIN in `cat domains_i_want_to_transfer` 269 | do 270 | # Carry out the transfer 271 | domainsale transfer --passphrase=my_secret_phrase --gasprice=20gwei --nonce=${NONCE} ${DOMAIN} && NONCE=$((NONCE+1)) 272 | done 273 | ``` 274 | 275 | There are two important points to note with this script. Firstly, it uses the `nonce` option and manually increments this after each successful operation. This is required as when sending a large number of transactions one after the other geth can become confused as to the current nonce when attempting to work it out automatically. Secondly, it provides an explicit gas price. This is important when carrying out bulk transactions as it ensures that the overall cost of operations is understood as well as the time for transactions to be mined. 276 | -------------------------------------------------------------------------------- /contracts/DomainSale.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.2; 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import "node_modules/wealdtech-solidity/contracts/ens/ENSReverseRegister.sol"; 16 | import "node_modules/wealdtech-solidity/contracts/math/SafeMath.sol"; 17 | import "node_modules/wealdtech-solidity/contracts/auth/Permissioned.sol"; 18 | import "node_modules/wealdtech-solidity/contracts/lifecycle/Pausable.sol"; 19 | 20 | // Interesting parts of the ENS deed 21 | contract Deed { 22 | address public owner; 23 | address public previousOwner; 24 | } 25 | 26 | // Interesting parts of the ENS registry 27 | contract Registry { 28 | function owner(bytes32 _hash) public constant returns (address); 29 | } 30 | 31 | // Interesting parts of the ENS registrar 32 | contract Registrar { 33 | function transfer(bytes32 _hash, address newOwner) public; 34 | function entries(bytes32 _hash) public constant returns (uint, Deed, uint, uint, uint); 35 | } 36 | 37 | contract DomainSale is ENSReverseRegister, Pausable { 38 | using SafeMath for uint256; 39 | 40 | Registrar public registrar; 41 | mapping (string => Sale) private sales; 42 | mapping (address => uint256) private balances; 43 | 44 | // Auction parameters 45 | uint256 private constant AUCTION_DURATION = 24 hours; 46 | uint256 private constant HIGH_BID_KICKIN = 7 days; 47 | uint256 private constant NORMAL_BID_INCREASE_PERCENTAGE = 10; 48 | uint256 private constant HIGH_BID_INCREASE_PERCENTAGE = 50; 49 | 50 | // Distribution of the sale funds 51 | uint256 private constant SELLER_SALE_PERCENTAGE = 90; 52 | uint256 private constant START_REFERRER_SALE_PERCENTAGE = 5; 53 | uint256 private constant BID_REFERRER_SALE_PERCENTAGE = 5; 54 | 55 | // ENS 56 | string private constant CONTRACT_ENS = "domainsale.eth"; 57 | // Hex is namehash("eth") 58 | bytes32 private constant NAMEHASH_ETH = 0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae; 59 | 60 | struct Sale { 61 | // The lowest direct purchase price that will be accepted 62 | uint256 price; 63 | // The lowest auction bid that will be accepted 64 | uint256 reserve; 65 | // The last bid on the auction. 0 if no bid has been made 66 | uint256 lastBid; 67 | // The address of the last bider on the auction. 0 if no bid has been made 68 | address lastBidder; 69 | // The timestamp when this auction started 70 | uint256 auctionStarted; 71 | // The timestamp at which this auction ends 72 | uint256 auctionEnds; 73 | // The address of the referrer who started the sale 74 | address startReferrer; 75 | // The address of the referrer who supplied the winning bid 76 | address bidReferrer; 77 | } 78 | 79 | // 80 | // Events 81 | // 82 | 83 | // Sent when a name is offered (can occur multiple times if the seller 84 | // changes their prices) 85 | event Offer(address indexed seller, string name, uint256 price, uint256 reserve); 86 | // Sent when a bid is placed for a name 87 | event Bid(address indexed bidder, string name, uint256 bid); 88 | // Sent when a name is transferred to a new owner 89 | event Transfer(address indexed seller, address indexed buyer, string name, uint256 value); 90 | // Sent when a sale for a name is cancelled 91 | event Cancel(string name); 92 | // Sent when funds are withdrawn 93 | event Withdraw(address indexed recipient, uint256 amount); 94 | 95 | // 96 | // Modifiers 97 | // 98 | 99 | // Actions that can only be undertaken by the seller of the name. 100 | // The owner of the name is this contract, so we use the previous 101 | // owner from the deed 102 | modifier onlyNameSeller(string _name) { 103 | Deed deed; 104 | (,deed,,,) = registrar.entries(keccak256(_name)); 105 | require(deed.owner() == address(this)); 106 | require(deed.previousOwner() == msg.sender); 107 | _; 108 | } 109 | 110 | // It is possible for a name to be invalidated, in which case the 111 | // owner will be reset 112 | modifier deedValid(string _name) { 113 | address deed; 114 | (,deed,,,) = registrar.entries(keccak256(_name)); 115 | require(deed != 0); 116 | _; 117 | } 118 | 119 | // Actions that can only be undertaken if the name sale has attracted 120 | // no bids. 121 | modifier auctionNotStarted(string _name) { 122 | require(sales[_name].auctionStarted == 0); 123 | _; 124 | } 125 | 126 | // Allow if the name can be bid upon 127 | modifier canBid(string _name) { 128 | require(sales[_name].reserve != 0); 129 | _; 130 | } 131 | 132 | // Allow if the name can be purchased 133 | modifier canBuy(string _name) { 134 | require(sales[_name].price != 0); 135 | _; 136 | } 137 | 138 | /** 139 | * @dev Constructor takes the address of the ENS registry 140 | */ 141 | function DomainSale(address _registry) public ENSReverseRegister(_registry, CONTRACT_ENS) { 142 | registrar = Registrar(Registry(_registry).owner(NAMEHASH_ETH)); 143 | } 144 | 145 | // 146 | // Accessors for sales struct 147 | // 148 | 149 | /** 150 | * @dev return useful information from the sale structure in one go 151 | */ 152 | function sale(string _name) public constant returns (uint256, uint256, uint256, address, uint256, uint256) { 153 | Sale storage s = sales[_name]; 154 | return (s.price, s.reserve, s.lastBid, s.lastBidder, s.auctionStarted, s.auctionEnds); 155 | } 156 | 157 | /** 158 | * @dev a flag set if this name can be purchased through auction 159 | */ 160 | function isAuction(string _name) public constant returns (bool) { 161 | return sales[_name].reserve != 0; 162 | } 163 | 164 | /** 165 | * @dev a flag set if this name can be purchased outright 166 | */ 167 | function isBuyable(string _name) public constant returns (bool) { 168 | return sales[_name].price != 0 && sales[_name].auctionStarted == 0; 169 | } 170 | 171 | /** 172 | * @dev a flag set if the auction has started 173 | */ 174 | function auctionStarted(string _name) public constant returns (bool) { 175 | return sales[_name].lastBid != 0; 176 | } 177 | 178 | /** 179 | * @dev the time at which the auction ends 180 | */ 181 | function auctionEnds(string _name) public constant returns (uint256) { 182 | return sales[_name].auctionEnds; 183 | } 184 | 185 | /** 186 | * @dev minimumBid is the greater of the minimum bid or the last bid + 10%. 187 | * If an auction has been going longer than 7 days then it is the last 188 | * bid + 50%. 189 | */ 190 | function minimumBid(string _name) public constant returns (uint256) { 191 | Sale storage s = sales[_name]; 192 | 193 | if (s.auctionStarted == 0) { 194 | return s.reserve; 195 | } else if (s.auctionStarted.add(HIGH_BID_KICKIN) > now) { 196 | return s.lastBid.add(s.lastBid.mul(NORMAL_BID_INCREASE_PERCENTAGE).div(100)); 197 | } else { 198 | return s.lastBid.add(s.lastBid.mul(HIGH_BID_INCREASE_PERCENTAGE).div(100)); 199 | } 200 | } 201 | 202 | /** 203 | * @dev price is the instant purchase price. 204 | */ 205 | function price(string _name) public constant returns (uint256) { 206 | return sales[_name].price; 207 | } 208 | 209 | /** 210 | * @dev The balance available for withdrawal 211 | */ 212 | function balance(address addr) public constant returns (uint256) { 213 | return balances[addr]; 214 | } 215 | 216 | // 217 | // Operations 218 | // 219 | 220 | /** 221 | * @dev offer a domain for sale. 222 | * The price is the price at which a domain can be purchased directly. 223 | * The reserve is the initial lowest price for which a bid can be made. 224 | */ 225 | function offer(string _name, uint256 _price, uint256 reserve, address referrer) onlyNameSeller(_name) auctionNotStarted(_name) deedValid(_name) ifNotPaused public { 226 | require(_price == 0 || _price > reserve); 227 | require(_price != 0 || reserve != 0); 228 | Sale storage s = sales[_name]; 229 | s.reserve = reserve; 230 | s.price = _price; 231 | s.startReferrer = referrer; 232 | Offer(msg.sender, _name, _price, reserve); 233 | } 234 | 235 | /** 236 | * @dev cancel a sale for a domain. 237 | * This can only happen if there have been no bids for the name. 238 | */ 239 | function cancel(string _name) onlyNameSeller(_name) auctionNotStarted(_name) deedValid(_name) ifNotPaused public { 240 | // Finished with the sale information 241 | delete sales[_name]; 242 | 243 | registrar.transfer(keccak256(_name), msg.sender); 244 | Cancel(_name); 245 | } 246 | 247 | /** 248 | * @dev buy a domain directly 249 | */ 250 | function buy(string _name, address bidReferrer) canBuy(_name) deedValid(_name) ifNotPaused public payable { 251 | Sale storage s = sales[_name]; 252 | require(msg.value >= s.price); 253 | require(s.auctionStarted == 0); 254 | 255 | // Obtain the previous owner from the deed 256 | Deed deed; 257 | (,deed,,,) = registrar.entries(keccak256(_name)); 258 | address previousOwner = deed.previousOwner(); 259 | 260 | // Transfer the name 261 | registrar.transfer(keccak256(_name), msg.sender); 262 | Transfer(previousOwner, msg.sender, _name, msg.value); 263 | 264 | // Distribute funds to referrers 265 | distributeFunds(msg.value, previousOwner, s.startReferrer, bidReferrer); 266 | 267 | // Finished with the sale information 268 | delete sales[_name]; 269 | 270 | // As we're here, return any funds that the sender is owed 271 | withdraw(); 272 | } 273 | 274 | /** 275 | * @dev bid for a domain 276 | */ 277 | function bid(string _name, address bidReferrer) canBid(_name) deedValid(_name) ifNotPaused public payable { 278 | require(msg.value >= minimumBid(_name)); 279 | 280 | Sale storage s = sales[_name]; 281 | require(s.auctionStarted == 0 || now < s.auctionEnds); 282 | 283 | if (s.auctionStarted == 0) { 284 | // First bid; set the auction start 285 | s.auctionStarted = now; 286 | } else { 287 | // Update the balance for the outbid bidder 288 | balances[s.lastBidder] = balances[s.lastBidder].add(s.lastBid); 289 | } 290 | s.lastBidder = msg.sender; 291 | s.lastBid = msg.value; 292 | s.auctionEnds = now.add(AUCTION_DURATION); 293 | s.bidReferrer = bidReferrer; 294 | Bid(msg.sender, _name, msg.value); 295 | 296 | // As we're here, return any funds that the sender is owed 297 | withdraw(); 298 | } 299 | 300 | /** 301 | * @dev finish an auction 302 | */ 303 | function finish(string _name) deedValid(_name) ifNotPaused public { 304 | Sale storage s = sales[_name]; 305 | require(now > s.auctionEnds); 306 | 307 | // Obtain the previous owner from the deed 308 | Deed deed; 309 | (,deed,,,) = registrar.entries(keccak256(_name)); 310 | 311 | address previousOwner = deed.previousOwner(); 312 | registrar.transfer(keccak256(_name), s.lastBidder); 313 | Transfer(previousOwner, s.lastBidder, _name, s.lastBid); 314 | 315 | // Distribute funds to referrers 316 | distributeFunds(s.lastBid, previousOwner, s.startReferrer, s.bidReferrer); 317 | 318 | // Finished with the sale information 319 | delete sales[_name]; 320 | 321 | // As we're here, return any funds that the sender is owed 322 | withdraw(); 323 | } 324 | 325 | /** 326 | * @dev withdraw any owned balance 327 | */ 328 | function withdraw() ifNotPaused public { 329 | uint256 amount = balances[msg.sender]; 330 | if (amount > 0) { 331 | balances[msg.sender] = 0; 332 | msg.sender.transfer(amount); 333 | Withdraw(msg.sender, amount); 334 | } 335 | } 336 | 337 | /** 338 | * @dev Invalidate an auction if the deed is no longer active 339 | */ 340 | function invalidate(string _name) ifNotPaused public { 341 | // Ensure the deed has been invalidated 342 | address deed; 343 | (,deed,,,) = registrar.entries(keccak256(_name)); 344 | require(deed == 0); 345 | 346 | Sale storage s = sales[_name]; 347 | 348 | // Update the balance for the winning bidder 349 | balances[s.lastBidder] = balances[s.lastBidder].add(s.lastBid); 350 | 351 | // Finished with the sale information 352 | delete sales[_name]; 353 | 354 | // Cancel the auction 355 | Cancel(_name); 356 | 357 | // As we're here, return any funds that the sender is owed 358 | withdraw(); 359 | } 360 | 361 | // 362 | // Internal functions 363 | // 364 | 365 | /** 366 | * @dev Distribute funds for a sale to the relevant parties 367 | */ 368 | function distributeFunds(uint256 amount, address seller, address startReferrer, address bidReferrer) internal { 369 | uint256 startReferrerFunds = amount.mul(START_REFERRER_SALE_PERCENTAGE).div(100); 370 | balances[startReferrer] = balances[startReferrer].add(startReferrerFunds); 371 | uint256 bidReferrerFunds = amount.mul(BID_REFERRER_SALE_PERCENTAGE).div(100); 372 | balances[bidReferrer] = balances[bidReferrer].add(bidReferrerFunds); 373 | uint256 sellerFunds = amount.sub(startReferrerFunds).sub(bidReferrerFunds); 374 | balances[seller] = balances[seller].add(sellerFunds); 375 | } 376 | } 377 | -------------------------------------------------------------------------------- /test/DomainSale.js: -------------------------------------------------------------------------------- 1 | const ENS = artifacts.require("./ENS.sol"); 2 | const MockEnsRegistrar = artifacts.require("./contracts/MockEnsRegistrar.sol"); 3 | const DomainSale = artifacts.require("./DomainSale.sol"); 4 | const Deed = artifacts.require("./Deed.sol"); 5 | const MaliciousSeller = artifacts.require("./contracts/MaliciousSeller.sol"); 6 | const MaliciousReferrer = artifacts.require("./contracts/MaliciousReferrer.sol"); 7 | 8 | const sha3 = require('solidity-sha3').default; 9 | const assertJump = require('./helpers/assertJump'); 10 | 11 | const increaseTime = addSeconds => web3.currentProvider.send({ jsonrpc: "2.0", method: "evm_increaseTime", params: [addSeconds], id: 0 }) 12 | const mine = () => web3.currentProvider.send({ jsonrpc: "2.0", method: "evm_mine", params: [], id: 0 }) 13 | 14 | const ethLabelHash = sha3('eth'); 15 | const ethNameHash = sha3('0x0000000000000000000000000000000000000000000000000000000000000000', ethLabelHash); 16 | const testdomain1LabelHash = sha3('testdomain1'); 17 | const testdomain1ethNameHash = sha3(ethNameHash, testdomain1LabelHash); 18 | const testdomain2LabelHash = sha3('testdomain2'); 19 | const testdomain2ethNameHash = sha3(ethNameHash, testdomain2LabelHash); 20 | const testdomain3LabelHash = sha3('testdomain3'); 21 | const testdomain3ethNameHash = sha3(ethNameHash, testdomain3LabelHash); 22 | const testdomain4LabelHash = sha3('testdomain4'); 23 | const testdomain4ethNameHash = sha3(ethNameHash, testdomain4LabelHash); 24 | const testdomain5LabelHash = sha3('testdomain5'); 25 | const testdomain5ethNameHash = sha3(ethNameHash, testdomain5LabelHash); 26 | const testdomain6LabelHash = sha3('testdomain6'); 27 | const testdomain6ethNameHash = sha3(ethNameHash, testdomain6LabelHash); 28 | const testdomain7LabelHash = sha3('testdomain7'); 29 | const testdomain7ethNameHash = sha3(ethNameHash, testdomain7LabelHash); 30 | const testdomain8LabelHash = sha3('testdomain8'); 31 | const testdomain8ethNameHash = sha3(ethNameHash, testdomain8LabelHash); 32 | 33 | 34 | contract('DomainSale', (accounts) => { 35 | const ensOwner = accounts[0]; 36 | const registrarOwner = accounts[1]; 37 | const domainSaleOwner = accounts[2]; 38 | const testdomainOwner = accounts[3]; 39 | const referrer1 = accounts[4]; 40 | const referrer2 = accounts[5]; 41 | const referrer3 = accounts[6]; 42 | const bidder1 = accounts[7]; 43 | const bidder2 = accounts[8]; 44 | 45 | // Carry ENS etc. over tests 46 | var registry; 47 | var registrar; 48 | // Carry DomainSale over tests 49 | var domainSale; 50 | 51 | it('should set up the registrar and test domains', async() => { 52 | registry = await ENS.deployed(); 53 | registrar = await MockEnsRegistrar.new(registry.address, ethNameHash, { from: registrarOwner, value: web3.toWei(10, 'ether') }); 54 | await registry.setSubnodeOwner("0x0", ethLabelHash, registrar.address); 55 | await registrar.register(testdomain1LabelHash, { from: testdomainOwner, value: web3.toWei(0.01, 'ether') }); 56 | await registrar.register(testdomain2LabelHash, { from: testdomainOwner, value: web3.toWei(0.01, 'ether') }); 57 | await registrar.register(testdomain3LabelHash, { from: testdomainOwner, value: web3.toWei(0.01, 'ether') }); 58 | await registrar.register(testdomain4LabelHash, { from: testdomainOwner, value: web3.toWei(0.01, 'ether') }); 59 | await registrar.register(testdomain5LabelHash, { from: testdomainOwner, value: web3.toWei(0.01, 'ether') }); 60 | await registrar.register(testdomain6LabelHash, { from: testdomainOwner, value: web3.toWei(0.01, 'ether') }); 61 | await registrar.register(testdomain7LabelHash, { from: testdomainOwner, value: web3.toWei(0.01, 'ether') }); 62 | await registrar.register(testdomain8LabelHash, { from: testdomainOwner, value: web3.toWei(0.01, 'ether') }); 63 | }); 64 | 65 | it('should offer a domain for sale', async() => { 66 | domainSale = await DomainSale.new(registry.address, { from: domainSaleOwner }); 67 | 68 | // Transfer deed ownership to the domain sale contract 69 | await registrar.transfer(testdomain1LabelHash, domainSale.address, { from: testdomainOwner }); 70 | // Ensure that the ownership is changed 71 | const entry = await registrar.entries(testdomain1LabelHash); 72 | assert.equal(await Deed.at(entry[1]).owner(), domainSale.address); 73 | 74 | // Ensure that the auction has not started 75 | assert.equal(await domainSale.auctionStarted('testdomain1'), false); 76 | 77 | // Set reserve and price for the domain 78 | await domainSale.offer('testdomain1', web3.toWei(1, 'ether'), web3.toWei(0.1, 'ether'), referrer1, { from: testdomainOwner }); 79 | 80 | // Ensure that the auction has not been triggered 81 | assert.equal(await domainSale.auctionStarted('testdomain1'), false); 82 | }); 83 | 84 | it('should obtain an immediate sale', async() => { 85 | // Ensure that the auction has not started 86 | assert.equal(await domainSale.auctionStarted('testdomain1'), false); 87 | 88 | const priorSellerBalance = await domainSale.balance(testdomainOwner); 89 | const priorReferrer1Balance = await domainSale.balance(referrer1); 90 | const priorReferrer2Balance = await domainSale.balance(referrer2); 91 | tx = await domainSale.buy('testdomain1', referrer2, { from: bidder1, value: web3.toWei(1, 'ether') }); 92 | console.log('Cost of buy is ' + tx.receipt.gasUsed); 93 | const currentSellerBalance = await domainSale.balance(testdomainOwner); 94 | const currentReferrer1Balance = await domainSale.balance(referrer1); 95 | const currentReferrer2Balance = await domainSale.balance(referrer2); 96 | 97 | // Ensure that the auction has not started 98 | assert.equal(await domainSale.auctionStarted('testdomain1'), false); 99 | 100 | // Ensure that the deed is now owned by the winner 101 | const entry = await registrar.entries(sha3('testdomain1')); 102 | assert.equal(await Deed.at(entry[1]).owner(), bidder1); 103 | assert.equal(await Deed.at(entry[1]).previousOwner(), domainSale.address); 104 | 105 | // Ensure that the seller has 90% of the sale price 106 | assert.equal(currentSellerBalance.sub(priorSellerBalance), web3.toWei(1, 'ether') * 0.9); 107 | // Ensure that the first referrer has 5% of the sale price 108 | assert.equal(currentReferrer1Balance.sub(priorReferrer1Balance), web3.toWei(1, 'ether') * 0.05); 109 | // Ensure that the second referrer has 5% of the sale price 110 | assert.equal(currentReferrer2Balance.sub(priorReferrer2Balance), web3.toWei(1, 'ether') * 0.05); 111 | }); 112 | 113 | it('should offer a domain for sale (2)', async() => { 114 | // Transfer deed ownership to the domain sale contract 115 | await registrar.transfer(testdomain2LabelHash, domainSale.address, { from: testdomainOwner }); 116 | // Ensure that the ownership is changed 117 | const entry = await registrar.entries(testdomain2LabelHash); 118 | assert.equal(await Deed.at(entry[1]).owner(), domainSale.address); 119 | 120 | // Ensure that the auction has not started 121 | assert.equal(await domainSale.auctionStarted('testdomain2'), false); 122 | 123 | // Set reserve and price for the domain 124 | await domainSale.offer('testdomain2', web3.toWei(1, 'ether'), web3.toWei(0.1, 'ether'), referrer1, { from: testdomainOwner }); 125 | 126 | // Ensure that the auction has not been triggered 127 | assert.equal(await domainSale.auctionStarted('testdomain2'), false); 128 | }); 129 | 130 | it('should refuse a bid under the minimum', async() => { 131 | await domainSale.offer('testdomain2', web3.toWei(0.1, 'ether'), web3.toWei(0.01, 'ether'), referrer1, { from: testdomainOwner }); 132 | try { 133 | await domainSale.bid('testdomain2', referrer2, { from: bidder1, value: web3.toWei(0.001, 'ether') }); 134 | assert.fail(); 135 | } catch (error) { 136 | assertJump(error); 137 | } 138 | }); 139 | 140 | it('should refuse to accept a buy when price is 0', async() => { 141 | await domainSale.offer('testdomain2', 0, web3.toWei(0.1, 'ether'), referrer1, { from: testdomainOwner }); 142 | try { 143 | await domainSale.buy('testdomain2', referrer2, { from: bidder1, value: web3.toWei(1, 'ether') }); 144 | assert.fail(); 145 | } catch (error) { 146 | assertJump(error); 147 | } 148 | }); 149 | 150 | it('should refuse to accept a bid when reserve is 0', async() => { 151 | await domainSale.offer('testdomain2', web3.toWei(1, 'ether'), 0, referrer1, { from: testdomainOwner }); 152 | try { 153 | await domainSale.bid('testdomain2', referrer2, { from: bidder1, value: web3.toWei(1, 'ether') }); 154 | assert.fail(); 155 | } catch (error) { 156 | assertJump(error); 157 | } 158 | }); 159 | 160 | it('should cancel an offer of a domain for sale', async() => { 161 | // Ensure that the auction has not started 162 | assert.equal(await domainSale.auctionStarted('testdomain2'), false); 163 | 164 | // Cancel the sale 165 | tx = await domainSale.cancel('testdomain2', { from: testdomainOwner }); 166 | console.log('Cost of cancel is ' + tx.receipt.gasUsed); 167 | 168 | // Ensure that the auction has not been triggered 169 | assert.equal(await domainSale.auctionStarted('testdomain2'), false); 170 | 171 | // Ensure that the ownership is changed 172 | const entry = await registrar.entries(testdomain2LabelHash); 173 | assert.equal(await Deed.at(entry[1]).owner(), testdomainOwner); 174 | }); 175 | 176 | it('should offer a domain for sale (3)', async() => { 177 | // Transfer deed ownership to the domain sale contract 178 | await registrar.transfer(testdomain2LabelHash, domainSale.address, { from: testdomainOwner }); 179 | // Ensure that the ownership is changed 180 | const entry = await registrar.entries(testdomain2LabelHash); 181 | assert.equal(await Deed.at(entry[1]).owner(), domainSale.address); 182 | 183 | // Ensure that the auction has not started 184 | assert.equal(await domainSale.auctionStarted('testdomain2'), false); 185 | 186 | // Set reserve and price for the domain 187 | await domainSale.offer('testdomain2', web3.toWei(1, 'ether'), web3.toWei(0.1, 'ether'), referrer1, { from: testdomainOwner }); 188 | 189 | // Ensure that the auction has not been triggered 190 | assert.equal(await domainSale.auctionStarted('testdomain2'), false); 191 | }); 192 | 193 | it('should accept bids', async() => { 194 | // Ensure that the auction has not started 195 | assert.equal(await domainSale.auctionStarted('testdomain2'), false); 196 | 197 | // Obtain the minimum bid 198 | var minimumBid = await domainSale.minimumBid('testdomain2'); 199 | assert.equal(minimumBid, web3.toWei(0.1, 'ether')); 200 | 201 | // Confirm that the name is buyable and biddable 202 | assert.equal(await domainSale.isBuyable('testdomain2'), true); 203 | assert.equal(await domainSale.isAuction('testdomain2'), true); 204 | 205 | // Bid from first bidder 206 | await domainSale.bid('testdomain2', referrer2, { from: bidder1, value: web3.toWei(0.1, 'ether') }); 207 | 208 | // Confirm that the name is no longer buyable but still biddable 209 | assert.equal(await domainSale.isBuyable('testdomain2'), false); 210 | assert.equal(await domainSale.isAuction('testdomain2'), true); 211 | 212 | // Ensure that the auction has started 213 | assert.equal(await domainSale.auctionStarted('testdomain2'), true); 214 | 215 | // Obtain the new minimum bid 216 | minimumBid = await domainSale.minimumBid('testdomain2'); 217 | assert.equal(minimumBid, web3.toWei(0.11, 'ether')); 218 | 219 | // Bid from second bidder 220 | await domainSale.bid('testdomain2', referrer2, { from: bidder2, value: web3.toWei(0.2, 'ether') }); 221 | 222 | // Obtain the new minimum bid 223 | minimumBid = await domainSale.minimumBid('testdomain2'); 224 | assert.equal(minimumBid, web3.toWei(0.22, 'ether')); 225 | }); 226 | 227 | it('should increase the minimum bid to +50% after a week', async() => { 228 | for (bid = 1; bid <= 8; bid++) { 229 | await domainSale.bid('testdomain2', referrer2, { from: bidder1, value: web3.toWei(bid, 'ether') }); 230 | increaseTime(86300); // Less than 1 day to keep the auction alive 231 | } 232 | // Mine to ensure that the latest time increase has taken hold 233 | mine(); 234 | 235 | // At this point the auction has been alive for more than 7 days so the minimum bid should be +50% 236 | minimumBid = await domainSale.minimumBid('testdomain2'); 237 | assert.equal(minimumBid, web3.toWei(12, 'ether')); 238 | }); 239 | 240 | it('should not allow bids after the auction ends', async() => { 241 | increaseTime(86401); // No bids for more than 1 day 242 | mine(); 243 | 244 | // Attempt to bid 245 | try { 246 | await domainSale.bid('testdomain2', referrer2, { from: bidder1, value: web3.toWei(bid, 'ether') }); 247 | assert.fail(); 248 | } catch (error) { 249 | assertJump(error); 250 | } 251 | }); 252 | 253 | it('should finish auctions correctly', async() => { 254 | const priorSellerBalance = await domainSale.balance(testdomainOwner); 255 | const priorReferrer1Balance = await domainSale.balance(referrer1); 256 | const priorReferrer2Balance = await domainSale.balance(referrer2); 257 | await domainSale.finish('testdomain2', { from: bidder2 }); 258 | console.log('Cost of finish is ' + tx.receipt.gasUsed); 259 | const currentSellerBalance = await domainSale.balance(testdomainOwner); 260 | const currentReferrer1Balance = await domainSale.balance(referrer1); 261 | const currentReferrer2Balance = await domainSale.balance(referrer2); 262 | 263 | // Ensure that the deed is now owned by the winner 264 | const entry = await registrar.entries(sha3('testdomain2')); 265 | assert.equal(await Deed.at(entry[1]).owner(), bidder1); 266 | assert.equal(await Deed.at(entry[1]).previousOwner(), domainSale.address); 267 | 268 | // Ensure that the seller has 90% of the sale price 269 | assert.equal(currentSellerBalance.sub(priorSellerBalance), web3.toWei(8, 'ether') * 0.9); 270 | // Ensure that the first referrer has 5% of the sale price 271 | assert.equal(currentReferrer1Balance.sub(priorReferrer1Balance), web3.toWei(8, 'ether') * 0.05); 272 | // Ensure that the second referrer has 5% of the sale price 273 | assert.equal(currentReferrer2Balance.sub(priorReferrer2Balance), web3.toWei(8, 'ether') * 0.05); 274 | }); 275 | 276 | it('should not allow changes to the offer after the auction ends', async() => { 277 | // Attempt to alter the offer 278 | try { 279 | await domainSale.offer('testdomain2', web3.toWei(2, 'ether'), web3.toWei(0.2, 'ether'), referrer1, { from: testdomainOwner }); 280 | assert.fail(); 281 | } catch (error) { 282 | assertJump(error); 283 | } 284 | }); 285 | 286 | it('should not allow changes to the offer after the auction ends', async() => { 287 | // Attempt to alter the offer 288 | try { 289 | await domainSale.offer('testdomain2', web3.toWei(2, 'ether'), web3.toWei(0.2, 'ether'), referrer1, { from: testdomainOwner }); 290 | assert.fail(); 291 | } catch (error) { 292 | assertJump(error); 293 | } 294 | }); 295 | 296 | it('should not allow cancelling the auction after the auction ends', async() => { 297 | // Attempt to cancel the auction 298 | try { 299 | await domainSale.cancel('testdomain2', { from: testdomainOwner }); 300 | assert.fail(); 301 | } catch (error) { 302 | assertJump(error); 303 | } 304 | }); 305 | 306 | // 307 | // Check value transfers 308 | // 309 | 310 | 311 | it('should return ether when a bidder repeat bids on a name', async() => { 312 | await registrar.transfer(testdomain4LabelHash, domainSale.address, { from: testdomainOwner }); 313 | await domainSale.offer('testdomain4', web3.toWei(1, 'ether'), web3.toWei(0.01, 'ether'), referrer1, { from: testdomainOwner }); 314 | 315 | // Initial cost should 0.01 ether (the bid) and gas 316 | const bidder1Funds1 = await web3.eth.getBalance(bidder1); 317 | tx = await domainSale.bid('testdomain4', referrer1, { from: bidder1, value: web3.toWei(0.01, 'ether') }); 318 | gasUsed = tx.receipt.gasUsed * 100000000000; 319 | expectedFunds = bidder1Funds1.minus(gasUsed).minus(web3.toWei(0.01, 'ether')); 320 | const bidder1Funds2 = await web3.eth.getBalance(bidder1); 321 | assert.equal(expectedFunds.toString(), bidder1Funds2.toString()); 322 | 323 | // Next bid is for 0.02 ether; cost should be 0.01 ether (difference between the two bids) and gas 324 | tx = await domainSale.bid('testdomain4', referrer1, { from: bidder1, value: web3.toWei(0.02, 'ether') }); 325 | gasUsed = tx.receipt.gasUsed * 100000000000; 326 | expectedFunds = bidder1Funds2.minus(gasUsed).minus(web3.toWei(0.01, 'ether')); 327 | const bidder1Funds3 = await web3.eth.getBalance(bidder1); 328 | assert.equal(expectedFunds.toString(), bidder1Funds3.toString()); 329 | 330 | // Next bid is for 0.1 ether; cost should be 0.08 ether (difference between the two bids) and gas 331 | tx = await domainSale.bid('testdomain4', referrer1, { from: bidder1, value: web3.toWei(0.1, 'ether') }); 332 | gasUsed = tx.receipt.gasUsed * 100000000000; 333 | expectedFunds = bidder1Funds3.minus(gasUsed).minus(web3.toWei(0.08, 'ether')); 334 | const bidder1Funds4 = await web3.eth.getBalance(bidder1); 335 | assert.equal(expectedFunds.toString(), bidder1Funds4.toString()); 336 | }); 337 | 338 | it('should return ether when a bidder bids on an unrelated name', async() => { 339 | await registrar.transfer(testdomain5LabelHash, domainSale.address, { from: testdomainOwner }); 340 | await domainSale.offer('testdomain5', web3.toWei(1, 'ether'), web3.toWei(0.01, 'ether'), referrer1, { from: testdomainOwner }); 341 | await registrar.transfer(testdomain6LabelHash, domainSale.address, { from: testdomainOwner }); 342 | await domainSale.offer('testdomain6', web3.toWei(1, 'ether'), web3.toWei(0.01, 'ether'), referrer1, { from: testdomainOwner }); 343 | 344 | // Bidder 1 is outbid by bidder 2 and so gains a balance 345 | await domainSale.bid('testdomain5', referrer1, { from: bidder1, value: web3.toWei(0.01, 'ether') }); 346 | await domainSale.bid('testdomain5', referrer1, { from: bidder2, value: web3.toWei(0.02, 'ether') }); 347 | bidder1Balance = await domainSale.balance(bidder1); 348 | assert.equal(bidder1Balance.toString(), web3.toWei(0.01, 'ether').toString()); 349 | 350 | // Bidder 1 bids on a different name and should obtain their balance back 351 | const bidder1Funds1 = await web3.eth.getBalance(bidder1); 352 | tx = await domainSale.bid('testdomain6', referrer1, { from: bidder1, value: web3.toWei(0.01, 'ether') }); 353 | gasUsed = tx.receipt.gasUsed * 100000000000; 354 | expectedFunds = bidder1Funds1.minus(gasUsed); 355 | const bidder1Funds2 = await web3.eth.getBalance(bidder1); 356 | assert.equal(expectedFunds.toString(), bidder1Funds2.toString()); 357 | assert.equal(bidder1Balance.toString(), web3.toWei(0.01, 'ether').toString()); 358 | }); 359 | 360 | it('should return balance when asked', async() => { 361 | // Bidder 1 is outbid by bidder 2 and so gains a balance 362 | await domainSale.bid('testdomain5', referrer1, { from: bidder1, value: web3.toWei(0.03, 'ether') }); 363 | await domainSale.bid('testdomain5', referrer1, { from: bidder2, value: web3.toWei(0.04, 'ether') }); 364 | bidder1Balance = await domainSale.balance(bidder1); 365 | assert.equal(bidder1Balance.toString(), web3.toWei(0.03, 'ether').toString()); 366 | 367 | // Bidder 1 withdraws the balance 368 | const bidder1Funds1 = await web3.eth.getBalance(bidder1); 369 | tx = await domainSale.withdraw({ from: bidder1 }); 370 | gasUsed = tx.receipt.gasUsed * 100000000000; 371 | expectedFunds = bidder1Funds1.minus(gasUsed).plus(web3.toWei(0.03, 'ether')); 372 | const bidder1Funds2 = await web3.eth.getBalance(bidder1); 373 | assert.equal(expectedFunds.toString(), bidder1Funds2.toString()); 374 | }); 375 | 376 | it('should handle sub-Wei values to referrers', async() => { 377 | await registrar.transfer(testdomain7LabelHash, domainSale.address, { from: testdomainOwner }); 378 | await domainSale.offer('testdomain7', web3.toWei(9, 'wei'), 0, referrer1, { from: testdomainOwner }); 379 | 380 | const priorSellerBalance = await domainSale.balance(testdomainOwner); 381 | const priorReferrer1Balance = await domainSale.balance(referrer1); 382 | const priorReferrer2Balance = await domainSale.balance(referrer2); 383 | await domainSale.buy('testdomain7', referrer2, { from: bidder1, value: web3.toWei(9, 'wei') }); 384 | const currentSellerBalance = await domainSale.balance(testdomainOwner); 385 | const currentReferrer1Balance = await domainSale.balance(referrer1); 386 | const currentReferrer2Balance = await domainSale.balance(referrer2); 387 | 388 | // Ensure that the deed is now owned by the winner 389 | const entry = await registrar.entries(sha3('testdomain7')); 390 | assert.equal(await Deed.at(entry[1]).owner(), bidder1); 391 | assert.equal(await Deed.at(entry[1]).previousOwner(), domainSale.address); 392 | 393 | // Ensure that the seller has 90% of the sale price (which rounds up to 9) 394 | assert.equal(currentSellerBalance.sub(priorSellerBalance), web3.toWei(9, 'wei')); 395 | // Ensure that the first referrer has 5% of the sale price (which rounds down to 0) 396 | assert.equal(currentReferrer1Balance.sub(priorReferrer1Balance), web3.toWei(0, 'wei')); 397 | // Ensure that the second referrer has 5% of the sale price (which rounds down to 0) 398 | assert.equal(currentReferrer2Balance.sub(priorReferrer2Balance), web3.toWei(0, 'wei')); 399 | }); 400 | 401 | it('should allow invalidation before an auction', async() => { 402 | const inv1LabelHash = sha3('inv1'); 403 | const inv1ethNameHash = sha3(ethNameHash, inv1LabelHash); 404 | await registrar.register(inv1LabelHash, { from: testdomainOwner, value: web3.toWei(0.01, 'ether') }); 405 | await registrar.transfer(inv1LabelHash, domainSale.address, { from: testdomainOwner }); 406 | await domainSale.offer('inv1', web3.toWei(1, 'ether'), web3.toWei(0.01, 'ether'), referrer1, { from: testdomainOwner }); 407 | await registrar.invalidate(inv1LabelHash); 408 | try { 409 | await domainSale.bid('inv1', referrer2, { from: bidder1, value: web3.toWei(0.1, 'ether') }); 410 | assert.fail(); 411 | } catch (error) { 412 | assertJump(error); 413 | } 414 | }); 415 | 416 | it('should allow invalidation during an auction (1)', async() => { 417 | const inv2LabelHash = sha3('inv2'); 418 | const inv2ethNameHash = sha3(ethNameHash, inv2LabelHash); 419 | await registrar.register(inv2LabelHash, { from: testdomainOwner, value: web3.toWei(0.01, 'ether') }); 420 | await registrar.transfer(inv2LabelHash, domainSale.address, { from: testdomainOwner }); 421 | await domainSale.offer('inv2', web3.toWei(1, 'ether'), web3.toWei(0.01, 'ether'), referrer1, { from: testdomainOwner }); 422 | await domainSale.bid('inv2', referrer2, { from: bidder1, value: web3.toWei(0.1, 'ether') }); 423 | await registrar.invalidate(inv2LabelHash); 424 | try { 425 | await domainSale.bid('inv2', referrer2, { from: bidder1, value: web3.toWei(0.2, 'ether') }); 426 | assert.fail(); 427 | } catch (error) { 428 | assertJump(error); 429 | // Invalidate by the bidder and ensure the bid balance is returned 430 | const bidder1Funds1 = await web3.eth.getBalance(bidder1); 431 | tx = await domainSale.invalidate('inv2', { from: bidder1 }); 432 | gasUsed = tx.receipt.gasUsed * 100000000000; 433 | expectedFunds = bidder1Funds1.minus(gasUsed).plus(web3.toWei(0.1, 'ether')); 434 | const bidder1Funds2 = await web3.eth.getBalance(bidder1); 435 | assert.equal(expectedFunds.toString(), bidder1Funds2.toString()); 436 | } 437 | }); 438 | 439 | it('should allow invalidation during an auction (2)', async() => { 440 | const inv2LabelHash = sha3('inv2'); 441 | const inv2ethNameHash = sha3(ethNameHash, inv2LabelHash); 442 | await registrar.register(inv2LabelHash, { from: testdomainOwner, value: web3.toWei(0.01, 'ether') }); 443 | await registrar.transfer(inv2LabelHash, domainSale.address, { from: testdomainOwner }); 444 | await domainSale.offer('inv2', web3.toWei(1, 'ether'), web3.toWei(0.01, 'ether'), referrer1, { from: testdomainOwner }); 445 | await domainSale.bid('inv2', referrer2, { from: bidder1, value: web3.toWei(0.1, 'ether') }); 446 | await registrar.invalidate(inv2LabelHash); 447 | try { 448 | await domainSale.bid('inv2', referrer2, { from: bidder1, value: web3.toWei(0.2, 'ether') }); 449 | assert.fail(); 450 | } catch (error) { 451 | assertJump(error); 452 | // Invalidate by third party 453 | await domainSale.invalidate('inv2', { from: bidder2 }); 454 | // Ensure that the bid balance is returned 455 | const bidder1Funds1 = await web3.eth.getBalance(bidder1); 456 | tx = await domainSale.withdraw({ from: bidder1 }); 457 | gasUsed = tx.receipt.gasUsed * 100000000000; 458 | expectedFunds = bidder1Funds1.minus(gasUsed).plus(web3.toWei(0.1, 'ether')); 459 | const bidder1Funds2 = await web3.eth.getBalance(bidder1); 460 | assert.equal(expectedFunds.toString(), bidder1Funds2.toString()); 461 | } 462 | }); 463 | 464 | it('should allow invalidation after an auction', async() => { 465 | const inv2LabelHash = sha3('inv2'); 466 | const inv2ethNameHash = sha3(ethNameHash, inv2LabelHash); 467 | await registrar.register(inv2LabelHash, { from: testdomainOwner, value: web3.toWei(0.01, 'ether') }); 468 | await registrar.transfer(inv2LabelHash, domainSale.address, { from: testdomainOwner }); 469 | await domainSale.offer('inv2', web3.toWei(1, 'ether'), web3.toWei(0.01, 'ether'), referrer1, { from: testdomainOwner }); 470 | await domainSale.bid('inv2', referrer2, { from: bidder1, value: web3.toWei(0.1, 'ether') }); 471 | 472 | increaseTime(86401); // No bids for more than 1 day 473 | mine(); 474 | await registrar.invalidate(inv2LabelHash); 475 | 476 | // Invalidate by the bidder and ensure the bid balance is returned 477 | const bidder1Funds1 = await web3.eth.getBalance(bidder1); 478 | tx = await domainSale.invalidate('inv2', { from: bidder1 }); 479 | gasUsed = tx.receipt.gasUsed * 100000000000; 480 | expectedFunds = bidder1Funds1.minus(gasUsed).plus(web3.toWei(0.1, 'ether')); 481 | const bidder1Funds2 = await web3.eth.getBalance(bidder1); 482 | assert.equal(expectedFunds.toString(), bidder1Funds2.toString()); 483 | }); 484 | 485 | it('should not finish an invalid auction', async() => { 486 | const inv2LabelHash = sha3('inv2'); 487 | const inv2ethNameHash = sha3(ethNameHash, inv2LabelHash); 488 | await registrar.register(inv2LabelHash, { from: testdomainOwner, value: web3.toWei(0.01, 'ether') }); 489 | await registrar.transfer(inv2LabelHash, domainSale.address, { from: testdomainOwner }); 490 | await domainSale.offer('inv2', web3.toWei(1, 'ether'), web3.toWei(0.01, 'ether'), referrer1, { from: testdomainOwner }); 491 | await domainSale.bid('inv2', referrer2, { from: bidder1, value: web3.toWei(0.1, 'ether') }); 492 | 493 | increaseTime(86401); // No bids for more than 1 day 494 | mine(); 495 | await registrar.invalidate(inv2LabelHash); 496 | 497 | // Attempt to finish the auction 498 | try { 499 | await domainSale.finish('inv2', { from: bidder2 }); 500 | assert.fail(); 501 | } catch (error) { 502 | await domainSale.invalidate('inv2', { from: bidder2 }); 503 | // Ensure that the bid balance is returned 504 | const bidder1Funds1 = await web3.eth.getBalance(bidder1); 505 | tx = await domainSale.withdraw({ from: bidder1 }); 506 | gasUsed = tx.receipt.gasUsed * 100000000000; 507 | expectedFunds = bidder1Funds1.minus(gasUsed).plus(web3.toWei(0.1, 'ether')); 508 | const bidder1Funds2 = await web3.eth.getBalance(bidder1); 509 | assert.equal(expectedFunds.toString(), bidder1Funds2.toString()); 510 | } 511 | }); 512 | 513 | it('should handle buying from a malicious seller', async() => { 514 | const maliciousSeller = await MaliciousSeller.new({ from: testdomainOwner, value: web3.toWei(2, 'ether') }); 515 | const malicious1LabelHash = sha3('malicious1'); 516 | const malicious1ethNameHash = sha3(ethNameHash, malicious1LabelHash); 517 | await registrar.register(malicious1LabelHash, { from: testdomainOwner, value: web3.toWei(0.01, 'ether') }); 518 | await registrar.transfer(malicious1LabelHash, maliciousSeller.address, { from: testdomainOwner }); 519 | await maliciousSeller.transfer(registrar.address, 'malicious1', domainSale.address, { from: testdomainOwner }); 520 | await maliciousSeller.offer(domainSale.address, 'malicious1', web3.toWei(1, 'ether'), web3.toWei(0.01, 'ether'), referrer1, { from: testdomainOwner }); 521 | await domainSale.buy('malicious1', referrer2, { from: bidder1, value: web3.toWei(1, 'ether') }); 522 | }); 523 | 524 | it('should handle auctioning from a malicious seller', async() => { 525 | const maliciousSeller = await MaliciousSeller.new({ from: testdomainOwner, value: web3.toWei(2, 'ether') }); 526 | const malicious2LabelHash = sha3('malicious2'); 527 | const malicious2ethNameHash = sha3(ethNameHash, malicious2LabelHash); 528 | await registrar.register(malicious2LabelHash, { from: testdomainOwner, value: web3.toWei(0.01, 'ether') }); 529 | await registrar.transfer(malicious2LabelHash, maliciousSeller.address, { from: testdomainOwner }); 530 | await maliciousSeller.transfer(registrar.address, 'malicious2', domainSale.address, { from: testdomainOwner }); 531 | await maliciousSeller.offer(domainSale.address, 'malicious2', web3.toWei(1, 'ether'), web3.toWei(0.01, 'ether'), referrer1, { from: testdomainOwner }); 532 | await domainSale.bid('malicious2', referrer2, { from: bidder1, value: web3.toWei(1, 'ether') }); 533 | 534 | increaseTime(86401); // No bids for more than 1 day 535 | mine(); 536 | await domainSale.finish('malicious2', { from: bidder2 }); 537 | }); 538 | 539 | it('should handle buying with a malicious referrer', async() => { 540 | const maliciousReferrer = await MaliciousReferrer.new({ from: testdomainOwner }); 541 | const malicious3LabelHash = sha3('malicious3'); 542 | const malicious3ethNameHash = sha3(ethNameHash, malicious3LabelHash); 543 | await registrar.register(malicious3LabelHash, { from: testdomainOwner, value: web3.toWei(0.01, 'ether') }); 544 | await registrar.transfer(malicious3LabelHash, domainSale.address, { from: testdomainOwner }); 545 | await domainSale.offer('malicious3', web3.toWei(1, 'ether'), web3.toWei(0.01, 'ether'), maliciousReferrer.address, { from: testdomainOwner }); 546 | await domainSale.bid('malicious3', maliciousReferrer.address, { from: bidder1, value: web3.toWei(0.1, 'ether') }); 547 | }); 548 | 549 | it('should handle auctioning from a malicious seller', async() => { 550 | const maliciousReferrer = await MaliciousReferrer.new({ from: testdomainOwner }); 551 | const malicious4LabelHash = sha3('malicious4'); 552 | const malicious4ethNameHash = sha3(ethNameHash, malicious4LabelHash); 553 | await registrar.register(malicious4LabelHash, { from: testdomainOwner, value: web3.toWei(0.01, 'ether') }); 554 | await registrar.transfer(malicious4LabelHash, domainSale.address, { from: testdomainOwner }); 555 | await domainSale.offer('malicious4', web3.toWei(1, 'ether'), web3.toWei(0.01, 'ether'), maliciousReferrer.address, { from: testdomainOwner }); 556 | await domainSale.bid('malicious4', maliciousReferrer.address, { from: bidder1, value: web3.toWei(0.1, 'ether') }); 557 | 558 | increaseTime(86401); // No bids for more than 1 day 559 | mine(); 560 | 561 | await domainSale.finish('malicious4', { from: bidder2 }); 562 | }); 563 | 564 | it('should not offer when paused', async() => { 565 | const testdomain9LabelHash = sha3('testdomain9'); 566 | const testdomain9ethNameHash = sha3(ethNameHash, testdomain9LabelHash); 567 | await registrar.register(testdomain9LabelHash, { from: testdomainOwner, value: web3.toWei(0.01, 'ether') }); 568 | await registrar.transfer(testdomain9LabelHash, domainSale.address, { from: testdomainOwner }); 569 | 570 | await domainSale.pause({from: domainSaleOwner}); 571 | try { 572 | await domainSale.offer('testdomain9', web3.toWei(1, 'ether'), web3.toWei(0.01, 'ether'), referrer1, { from: testdomainOwner }); 573 | assert.fail(); 574 | } catch (error) { 575 | // Okay 576 | } finally { 577 | await domainSale.unpause({from: domainSaleOwner}); 578 | } 579 | }); 580 | 581 | it('should not buy when paused', async() => { 582 | await domainSale.offer('testdomain9', web3.toWei(1, 'ether'), web3.toWei(0.01, 'ether'), referrer1, { from: testdomainOwner }); 583 | 584 | await domainSale.pause({from: domainSaleOwner}); 585 | try { 586 | await domainSale.buy('testdomain9', referrer1, { from: bidder1, value: web3.toWei(1, 'ether') }); 587 | assert.fail(); 588 | } catch (error) { 589 | // Okay 590 | } finally { 591 | await domainSale.unpause({from: domainSaleOwner}); 592 | } 593 | await domainSale.buy('testdomain9', referrer1, { from: bidder1, value: web3.toWei(1, 'ether') }); 594 | }); 595 | 596 | it('should not bid when paused', async() => { 597 | const testdomain10LabelHash = sha3('testdomain10'); 598 | const testdomain10ethNameHash = sha3(ethNameHash, testdomain10LabelHash); 599 | await registrar.register(testdomain10LabelHash, { from: testdomainOwner, value: web3.toWei(0.01, 'ether') }); 600 | await registrar.transfer(testdomain10LabelHash, domainSale.address, { from: testdomainOwner }); 601 | await domainSale.offer('testdomain10', web3.toWei(1, 'ether'), web3.toWei(0.01, 'ether'), referrer1, { from: testdomainOwner }); 602 | 603 | await domainSale.bid('testdomain10', referrer1, { from: bidder1, value: web3.toWei(0.1, 'ether') }); 604 | await domainSale.pause({from: domainSaleOwner}); 605 | try { 606 | await domainSale.bid('testdomain10', referrer1, { from: bidder2, value: web3.toWei(0.2, 'ether') }); 607 | assert.fail(); 608 | } catch (error) { 609 | // Okay 610 | } finally { 611 | await domainSale.unpause({from: domainSaleOwner}); 612 | } 613 | await domainSale.bid('testdomain10', referrer1, { from: bidder2, value: web3.toWei(0.2, 'ether') }); 614 | }); 615 | 616 | it('should not finish when paused', async() => { 617 | increaseTime(86401); // No bids for more than 1 day 618 | mine(); 619 | 620 | await domainSale.pause({from: domainSaleOwner}); 621 | try { 622 | await domainSale.finish('testdomain10', { from: bidder2 }); 623 | assert.fail(); 624 | } catch (error) { 625 | // Okay 626 | } finally { 627 | await domainSale.unpause({from: domainSaleOwner}); 628 | } 629 | await domainSale.finish('testdomain10', { from: bidder2 }); 630 | }); 631 | 632 | it('should not withdraw when paused', async() => { 633 | 634 | await domainSale.pause({from: domainSaleOwner}); 635 | try { 636 | await domainSale.withdraw({ from: testdomainOwner }); 637 | assert.fail(); 638 | } catch (error) { 639 | // Okay 640 | } finally { 641 | await domainSale.unpause({from: domainSaleOwner}); 642 | } 643 | await domainSale.withdraw({ from: testdomainOwner }); 644 | }); 645 | 646 | it('should not allow non-owner to pause', async() => { 647 | try { 648 | await domainSale.pause({from: testdomainOwner }); 649 | assert.fail(); 650 | } catch (error) { 651 | // Okay 652 | } 653 | }); 654 | }); 655 | --------------------------------------------------------------------------------