commit 8a9462b86888fe3c1445150f1b1d5977eba502eb Author: Daniel Perez Date: Mon Feb 13 01:06:14 2023 +0000 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..85198aa --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# Compiler files +cache/ +out/ + +# Ignores development broadcast logs +!/broadcast +/broadcast/*/31337/ +/broadcast/**/dry-run/ + +# Docs +docs/ + +# Dotenv file +.env diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..b2061c7 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "lib/forge-std"] + path = lib/forge-std + url = https://github.com/foundry-rs/forge-std + branch = v1.3.0 diff --git a/README.md b/README.md new file mode 100644 index 0000000..df26da9 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +# Coursework skeleton + +This is the skeleton for the coursework of the Principle of Distributed Ledgers 2023. +It contains [the interfaces](./src/interfaces) of the contracts to implement and an [ERC20 implementation](./src/contracts/PurchaseToken.sol). + +The repository uses [Foundry](https://book.getfoundry.sh/projects/working-on-an-existing-project). diff --git a/foundry.toml b/foundry.toml new file mode 100644 index 0000000..825eb62 --- /dev/null +++ b/foundry.toml @@ -0,0 +1,7 @@ +[profile.default] +src = 'src' +out = 'out' +libs = ['lib'] +solc = "0.8.10" + +# See more config options https://github.com/foundry-rs/foundry/tree/master/config diff --git a/lib/forge-std b/lib/forge-std new file mode 160000 index 0000000..066ff16 --- /dev/null +++ b/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 066ff16c5c03e6f931cd041fd366bc4be1fae82a diff --git a/lib/lib.sol b/lib/lib.sol new file mode 100644 index 0000000..e69de29 diff --git a/src/contracts/PurchaseToken.sol b/src/contracts/PurchaseToken.sol new file mode 100644 index 0000000..37f224e --- /dev/null +++ b/src/contracts/PurchaseToken.sol @@ -0,0 +1,451 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC20/ERC20.sol) +pragma solidity ^0.8.10; + +import "../interfaces/IERC20.sol"; + +/** + * @dev Provides information about the current execution context, including the + * sender of the transaction and its data. While these are generally available + * via msg.sender and msg.data, they should not be accessed in such a direct + * manner, since when dealing with meta-transactions the account sending and + * paying for execution may not be the actual sender (as far as an application + * is concerned). + * + * This contract is only required for intermediate, library-like contracts. + */ +abstract contract Context { + function _msgSender() internal view virtual returns (address) { + return msg.sender; + } + + function _msgData() internal view virtual returns (bytes calldata) { + return msg.data; + } +} + +/** + * @dev Implementation of the {IERC20} interface. + * + * This implementation is agnostic to the way tokens are created. This means + * that a supply mechanism has to be added in a derived contract using {_mint}. + * For a generic mechanism see {ERC20PresetMinterPauser}. + * + * TIP: For a detailed writeup see our guide + * https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[How + * to implement supply mechanisms]. + * + * The default value of {decimals} is 18. To change this, you should override + * this function so it returns a different value. + * + * We have followed general OpenZeppelin Contracts guidelines: functions revert + * instead returning `false` on failure. This behavior is nonetheless + * conventional and does not conflict with the expectations of ERC20 + * applications. + * + * Additionally, an {Approval} event is emitted on calls to {transferFrom}. + * This allows applications to reconstruct the allowance for all accounts just + * by listening to said events. Other implementations of the EIP may not emit + * these events, as it isn't required by the specification. + * + * Finally, the non-standard {decreaseAllowance} and {increaseAllowance} + * functions have been added to mitigate the well-known issues around setting + * allowances. See {IERC20-approve}. + */ +contract PurchaseToken is Context, IERC20, IERC20Metadata { + mapping(address => uint256) private _balances; + + mapping(address => mapping(address => uint256)) private _allowances; + + uint256 private _totalSupply; + + string private _name; + string private _symbol; + + /** + * @dev Sets the values for {name} and {symbol}. + * + * All two of these values are immutable: they can only be set once during + * construction. + */ + constructor() { + _name = "PurchaseToken"; + _symbol = "PT"; + } + + /** + * @dev Returns the name of the token. + */ + function name() public view virtual override returns (string memory) { + return _name; + } + + /** + * @dev Returns the symbol of the token, usually a shorter version of the + * name. + */ + function symbol() public view virtual override returns (string memory) { + return _symbol; + } + + /** + * @dev Returns the number of decimals used to get its user representation. + * For example, if `decimals` equals `18`, a balance of `5e18` tokens should + * be displayed to a user as `5.00` (`5e18 / 10 ** 18`). + * + * Tokens usually opt for a value of 18, imitating the relationship between + * Ether and Wei. This is the default value returned by this function, unless + * it's overridden. + * + */ + function decimals() public view virtual override returns (uint8) { + return 18; + } + + /** + * @dev See {IERC20-totalSupply}. + */ + function totalSupply() public view virtual override returns (uint256) { + return _totalSupply; + } + + /** + * @dev See {IERC20-balanceOf}. + */ + function balanceOf(address account) + public + view + virtual + override + returns (uint256) + { + return _balances[account]; + } + + /** + * @dev This allows users to mint ERC20 tokens by sending ETH to this contract. + * The amount of ERC20 tokens minted will be 100 times the amount of ETH sent. + */ + function mint() external payable { + uint256 amountToMint = msg.value * 100; + _mint(msg.sender, amountToMint); + } + + /** + * @dev See {IERC20-transfer}. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - the caller must have a balance of at least `amount`. + */ + function transfer(address to, uint256 amount) + public + virtual + override + returns (bool) + { + address owner = _msgSender(); + _transfer(owner, to, amount); + return true; + } + + /** + * @dev See {IERC20-allowance}. + */ + function allowance(address owner, address spender) + public + view + virtual + override + returns (uint256) + { + return _allowances[owner][spender]; + } + + /** + * @dev See {IERC20-approve}. + * + * NOTE: If `amount` is the maximum `uint256`, the allowance is not updated on + * `transferFrom`. This is semantically equivalent to an infinite approval. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function approve(address spender, uint256 amount) + public + virtual + override + returns (bool) + { + address owner = _msgSender(); + _approve(owner, spender, amount); + return true; + } + + /** + * @dev See {IERC20-transferFrom}. + * + * Emits an {Approval} event indicating the updated allowance. This is not + * required by the EIP. See the note at the beginning of {ERC20}. + * + * NOTE: Does not update the allowance if the current allowance + * is the maximum `uint256`. + * + * Requirements: + * + * - `from` and `to` cannot be the zero address. + * - `from` must have a balance of at least `amount`. + * - the caller must have allowance for ``from``'s tokens of at least + * `amount`. + */ + function transferFrom( + address from, + address to, + uint256 amount + ) public virtual override returns (bool) { + address spender = _msgSender(); + _spendAllowance(from, spender, amount); + _transfer(from, to, amount); + return true; + } + + /** + * @dev Atomically increases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function increaseAllowance(address spender, uint256 addedValue) + public + virtual + returns (bool) + { + address owner = _msgSender(); + _approve(owner, spender, allowance(owner, spender) + addedValue); + return true; + } + + /** + * @dev Atomically decreases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `spender` must have allowance for the caller of at least + * `subtractedValue`. + */ + function decreaseAllowance(address spender, uint256 subtractedValue) + public + virtual + returns (bool) + { + address owner = _msgSender(); + uint256 currentAllowance = allowance(owner, spender); + require( + currentAllowance >= subtractedValue, + "ERC20: decreased allowance below zero" + ); + unchecked { + _approve(owner, spender, currentAllowance - subtractedValue); + } + + return true; + } + + /** + * @dev Moves `amount` of tokens from `from` to `to`. + * + * This internal function is equivalent to {transfer}, and can be used to + * e.g. implement automatic token fees, slashing mechanisms, etc. + * + * Emits a {Transfer} event. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `from` must have a balance of at least `amount`. + */ + function _transfer( + address from, + address to, + uint256 amount + ) internal virtual { + require(from != address(0), "ERC20: transfer from the zero address"); + require(to != address(0), "ERC20: transfer to the zero address"); + + _beforeTokenTransfer(from, to, amount); + + uint256 fromBalance = _balances[from]; + require( + fromBalance >= amount, + "ERC20: transfer amount exceeds balance" + ); + unchecked { + _balances[from] = fromBalance - amount; + // Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by + // decrementing then incrementing. + _balances[to] += amount; + } + + emit Transfer(from, to, amount); + + _afterTokenTransfer(from, to, amount); + } + + /** @dev Creates `amount` tokens and assigns them to `account`, increasing + * the total supply. + * + * Emits a {Transfer} event with `from` set to the zero address. + * + * Requirements: + * + * - `account` cannot be the zero address. + */ + function _mint(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: mint to the zero address"); + + _beforeTokenTransfer(address(0), account, amount); + + _totalSupply += amount; + unchecked { + // Overflow not possible: balance + amount is at most totalSupply + amount, which is checked above. + _balances[account] += amount; + } + emit Transfer(address(0), account, amount); + + _afterTokenTransfer(address(0), account, amount); + } + + /** + * @dev Destroys `amount` tokens from `account`, reducing the + * total supply. + * + * Emits a {Transfer} event with `to` set to the zero address. + * + * Requirements: + * + * - `account` cannot be the zero address. + * - `account` must have at least `amount` tokens. + */ + function _burn(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: burn from the zero address"); + + _beforeTokenTransfer(account, address(0), amount); + + uint256 accountBalance = _balances[account]; + require(accountBalance >= amount, "ERC20: burn amount exceeds balance"); + unchecked { + _balances[account] = accountBalance - amount; + // Overflow not possible: amount <= accountBalance <= totalSupply. + _totalSupply -= amount; + } + + emit Transfer(account, address(0), amount); + + _afterTokenTransfer(account, address(0), amount); + } + + /** + * @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens. + * + * This internal function is equivalent to `approve`, and can be used to + * e.g. set automatic allowances for certain subsystems, etc. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `owner` cannot be the zero address. + * - `spender` cannot be the zero address. + */ + function _approve( + address owner, + address spender, + uint256 amount + ) internal virtual { + require(owner != address(0), "ERC20: approve from the zero address"); + require(spender != address(0), "ERC20: approve to the zero address"); + + _allowances[owner][spender] = amount; + emit Approval(owner, spender, amount); + } + + /** + * @dev Updates `owner` s allowance for `spender` based on spent `amount`. + * + * Does not update the allowance amount in case of infinite allowance. + * Revert if not enough allowance is available. + * + * Might emit an {Approval} event. + */ + function _spendAllowance( + address owner, + address spender, + uint256 amount + ) internal virtual { + uint256 currentAllowance = allowance(owner, spender); + if (currentAllowance != type(uint256).max) { + require( + currentAllowance >= amount, + "ERC20: insufficient allowance" + ); + unchecked { + _approve(owner, spender, currentAllowance - amount); + } + } + } + + /** + * @dev Hook that is called before any transfer of tokens. This includes + * minting and burning. + * + * Calling conditions: + * + * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens + * will be transferred to `to`. + * - when `from` is zero, `amount` tokens will be minted for `to`. + * - when `to` is zero, `amount` of ``from``'s tokens will be burned. + * - `from` and `to` are never both zero. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _beforeTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual {} + + /** + * @dev Hook that is called after any transfer of tokens. This includes + * minting and burning. + * + * Calling conditions: + * + * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens + * has been transferred to `to`. + * - when `from` is zero, `amount` tokens have been minted for `to`. + * - when `to` is zero, `amount` of ``from``'s tokens have been burned. + * - `from` and `to` are never both zero. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _afterTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual {} +} diff --git a/src/interfaces/IERC20.sol b/src/interfaces/IERC20.sol new file mode 100644 index 0000000..229d527 --- /dev/null +++ b/src/interfaces/IERC20.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC20/IERC20.sol) +pragma solidity ^0.8.10; + +/** + * @dev Interface of the ERC20 standard as defined in the EIP. + */ +interface IERC20 { + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval( + address indexed owner, + address indexed spender, + uint256 value + ); + + /** + * @dev Returns the amount of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the amount of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves `amount` tokens from the caller's account to `to`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address to, uint256 amount) external returns (bool); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) + external + view + returns (uint256); + + /** + * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 amount) external returns (bool); + + /** + * @dev Moves `amount` tokens from `from` to `to` using the + * allowance mechanism. `amount` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom( + address from, + address to, + uint256 amount + ) external returns (bool); +} + +// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC20/extensions/IERC20Metadata.sol) +interface IERC20Metadata is IERC20 { + /** + * @dev Returns the name of the token. + */ + function name() external view returns (string memory); + + /** + * @dev Returns the symbol of the token. + */ + function symbol() external view returns (string memory); + + /** + * @dev Returns the decimals places of the token. + */ + function decimals() external view returns (uint8); +} diff --git a/src/interfaces/IPrimaryMarket.sol b/src/interfaces/IPrimaryMarket.sol new file mode 100644 index 0000000..7979fb3 --- /dev/null +++ b/src/interfaces/IPrimaryMarket.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.10; + +/** + * @dev Required interface for the primary market. + * The primary market is the first point of sale for tickets. + * It is responsible for minting tickets and transferring them to the purchaser. + * In this implementation, the purchase price is fixed at 100e18 purchase tokens + * and the maximum number of tickets that can be purchased is 1000. + * The purchase token is an ERC20 token that is specified when the contract is deployed. + * The NFT to be minted is an implementation of the ITicketNFT interface and should be created (i.e. deployed) + * when the contract implementing this interface is deployed. + */ +interface IPrimaryMarket { + /** + * @dev Emitted when a purchase by `holder` occurs, with `holderName` specified. + */ + event Purchase(address indexed holder, string indexed holderName); + + /** + * @dev Returns the administrator of the primary market. + * This should be the address that created the contract. + */ + function admin() external view returns (address); + + /** + * @dev Takes the initial NFT token holder's name as a string input + * and transfers ERC20 tokens from the purchaser to the admin of this contract + */ + function purchase(string memory holderName) external; +} diff --git a/src/interfaces/ISecondaryMarket.sol b/src/interfaces/ISecondaryMarket.sol new file mode 100644 index 0000000..a1f78c1 --- /dev/null +++ b/src/interfaces/ISecondaryMarket.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.10; + +/** + * @dev Required interface for the secondary market. + * The secondary market is the point of sale for tickets after they have been initially purchased from the primary market + */ +interface ISecondaryMarket { + /** + * @dev Event emitted when a new ticket listing is created + */ + event Listing( + uint256 indexed ticketID, + address indexed holder, + uint256 price + ); + + /** + * @dev Event emitted when an amount of the purchase token is transferred from + */ + event Purchase( + address indexed purchaser, + uint256 indexed ticketID, + uint256 price, + string newName + ); + /** + * @dev Event emitted when a ticket is delisted + */ + event Delisting(uint256 indexed ticketID); + + /** + * @dev This method lists a ticket with `ticketID` for sale by transferring the ticket + * such that it is held by this contract. Only the current owner of a specific + * ticket is able to list that ticket on the secondary market. The purchase + * `price` is specified in an amount of `PurchaseToken`. + */ + function listTicket(uint256 ticketID, uint256 price) external; + + /** @dev This method allows the msg.sender to purchase a listed ticket with `ticketID` + * by paying the purchase price that was specified when the ticket was listed. + * `name` gives the new name that should be stated on the ticket when it is purchased. + * Note: Only non-expired and unused tickets can be purchased and there is a + * fee charged every time a purchase is made. The fee is charged on the price. + * The final amount that the lister of the ticket receives is the price + * minus the fee. The fee should go to the admin of the primary market. + */ + function purchase(uint256 ticketID, string calldata name) external; + + /** @dev This method delists a previously listed ticket with `ticketID`. Only the account that + * listed the ticket may delist the ticket. The ticket should be transferred back + * to msg.sender, i.e., the lister. + */ + function delistTicket(uint256 ticketID) external; +} diff --git a/src/interfaces/ITicketNFT.sol b/src/interfaces/ITicketNFT.sol new file mode 100644 index 0000000..c07dd02 --- /dev/null +++ b/src/interfaces/ITicketNFT.sol @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.10; + +/** + * @dev Required interface for the TicketNFT contract. + * A ticket NFT is a non-fungible token that represents a single entry to an event. + */ +interface ITicketNFT { + /** + * @dev Emitted when `ticketID` ticket is transferred from `from` to `to`. + */ + event Transfer( + address indexed from, + address indexed to, + uint256 indexed ticketID + ); + + /** + * @dev Emitted when `holder` enables `approved` to manage the `ticketID` ticket. + */ + event Approval( + address indexed holder, + address indexed approved, + uint256 indexed ticketID + ); + + /** + * Mints a new ticket for `holder` with `holderName`. + * The ticket must be assigned the following metadata: + * - A unique ticket ID. Once a ticket has been used or expired, its ID should not be reallocated + * - An expiry time of 10 days from the time of minting + * - A boolean `used` flag set to false + * On minting, a `Transfer` event should be emitted with `from` set to the zero address. + * + * Requirements: + * + * - The caller must be the primary market + */ + function mint(address holder, string memory holderName) external; + + /** + * @dev Returns the number of tickets a `holder` has. + */ + function balanceOf(address holder) external view returns (uint256 balance); + + /** + * @dev Returns the address of the holder of the `ticketID` ticket. + * + * Requirements: + * + * - `ticketID` must exist. + */ + function holderOf(uint256 ticketID) external view returns (address holder); + + /** + * @dev Transfers `ticketID` ticket from `from` to `to`. + * This should also set the approved address for this ticket to the zero address + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `ticketID` ticket must be either: + * - owned by `from`. + * - approved to move this ticket by `approve` + * + * Emits a `Transfer` and an `Approval` event. + */ + function transferFrom( + address from, + address to, + uint256 ticketID + ) external; + + /** + * @dev Gives permission to `to` to transfer `ticketID` ticket to another account. + * The approval is cleared when the ticket is transferred. + * + * Only a single account can be approved at a time, so approving the zero address clears previous approvals. + * + * Requirements: + * + * - The caller must own the ticket + * - `ticketID` must exist. + * + * Emits an `Approval` event. + */ + function approve(address to, uint256 ticketID) external; + + /** + * @dev Returns the account approved for `ticketID` ticket. + * + * Requirements: + * + * - `ticketID` must exist. + */ + function getApproved(uint256 ticketID) + external + view + returns (address operator); + + /** + * @dev Returns the current `holderName` associated with a `ticketID`. + * Requirements: + * + * - `ticketID` must exist. + */ + function holderNameOf(uint256 ticketID) + external + view + returns (string memory holderName); + + /** + * @dev Updates the `holderName` associated with a `ticketID`. + * Note that this does not update the actual holder of the ticket. + * + * Requirements: + * + * - `ticketID` must exists + * - Only the current holder can call this function + */ + function updateHolderName(uint256 ticketID, string calldata newName) + external; + + /** + * @dev Sets the `used` flag associated with a `ticketID` to `true` + * + * Requirements: + * + * - `ticketID` must exist + * - the ticket must not already be used + * - the ticket must not be expired + * - Only the administrator of the primary market can call this function + */ + function setUsed(uint256 ticketID) external; + + /** + * @dev Returns `true` if the `used` flag associated with a `ticketID` if `true` + * or if the ticket has expired, i.e., the current time is greater than the ticket's + * `expiryDate`. + * Requirements: + * + * - `ticketID` must exist + */ + function isExpiredOrUsed(uint256 ticketID) external view returns (bool); +} diff --git a/test/.keep b/test/.keep new file mode 100644 index 0000000..e69de29