From df800f05416aa11bd04501b6dd771307d05dc8b7 Mon Sep 17 00:00:00 2001 From: Santo Cariotti Date: Mon, 20 Jun 2022 14:53:17 +0200 Subject: Add exercises --- Year_3/Blockchain/CorporateManagement.sol | 188 ++++++++++++++++++++++++++++++ Year_3/Blockchain/RandomMoney.sol | 43 +++++++ Year_3/Blockchain/RockPaperScissor.sol | 124 ++++++++++++++++++++ Year_3/Blockchain/TicTacToe.sol | 153 ++++++++++++++++++++++++ 4 files changed, 508 insertions(+) create mode 100644 Year_3/Blockchain/CorporateManagement.sol create mode 100644 Year_3/Blockchain/RandomMoney.sol create mode 100644 Year_3/Blockchain/RockPaperScissor.sol create mode 100644 Year_3/Blockchain/TicTacToe.sol diff --git a/Year_3/Blockchain/CorporateManagement.sol b/Year_3/Blockchain/CorporateManagement.sol new file mode 100644 index 0000000..cb74871 --- /dev/null +++ b/Year_3/Blockchain/CorporateManagement.sol @@ -0,0 +1,188 @@ +/* + Corso Blockchain e Cryptocurrencies - Compito di laboratorio del 12/07/2021 + + Scrivere un contratto `CorporateManagement` che implementi l'interfaccia `CorporateManagementSpecs` + per la gestione delle decisioni di una società sulla piattaforma Ethereum usando il linguaggio Solidity. + + All'interno della società ci possono essere una serie di soci (associates): il primo è il fondatore che + crea il contratto stesso con relativa quota (share). Gli altri soci si possono auto-candidare depositando + una quota sufficiente: tale candidatura viene considerata accettata solo se accettata a maggioranza dai + soci attuali. Ogni socio può depositare una generica proposta, consistente in una semplice descrizione + testuale: essa sarà considerata accettata se votata a maggioranza dai soci. Il voto per maggioranza (50% + 1) + tiene conto del peso della quota in ETH depositata da ogni socio. + La società, su proposta di qualche socio, può essere anche sciolta purché la risoluzione venga votata + all'unanimità. + + Di seguito alcuni dettagli sui singoli elementi obbligatori dell'interfaccia: + - tutti i casi illustrati sopra vengono considerti come proposte da accettare: le tipologie di proposte + corrispondono all'enumerazione `ProposalCategory`; + - il primo socio (fondatore) crea il contratto specificando la quota minima e versando, contestualmente, la + propria di quota sufficiente; + + X - il metodo `depositFunds` può essere usato da chiunque per auto-candidarsi a socio, versando la relativa + sufficiente quota; il metodo può essere usato sia dai soci che da candidati-soci per aumentare la propria + quota; + X - il metodo `voteProposal`, utilizzabile solo dai soci effettivi, può essere usato per aderire, con la propria + quota, all'accettazione della proposta tramite l'apposito identificativo; ovviamente un socio può votare una + sola volta per ogni singola proposta, tale metodo dovrà anche verificare se la proposta ha raggiunto i voti + necessari e far scaturire i relativi effetti; + X - i metodi `depositGenericProposal` e `depositDissolutionProposal` permettono, unicamente ai soci, di depositare, + rispettivamente, una generica proposta con descrizione testuale o una proposta di scioglimento; + - nel caso in cui una proposta di scioglimento venga accettata all'unanimità la società va in liquidazione e pertanto + nessuna funzionalità avrà più effetti ad eccezione del metodo `requestShareRefunding` che permette al singolo + socio di riscuotere la propria quota; + - i predicati `isAssociated` e `isDissoluted` permettono, rispettivamente, di verificare se un indirizzo specificato + corrisponde ad un socio effettivo o se la società è in scioglimento; + + - gli eventi di tipo `New...` dovranno essere generati quando una proposta viene creata a seguito di un'azione + esterna; depositare dei fondi sufficienti da parte di un non-socio rappresenta una proposta di candidatura; + - gli eventi di tipo `Accepted....` dovranno essere generati ogniqualvolta una relativa proposta viene accettata; + notare che all'atto della creazione della società il socio fondatore è implicitamente accettato. + + Il contratto dovrà occuparsi di validare gli input ricevuti secondo criteri ovvi di sensatezza. + +*/ + +// SPDX-License-Identifier: None + +pragma solidity ^0.8.0; +import "exam.sol"; + +contract CorporateManagement is CorporateManagementSpecs { + enum AssociateRole { NONE, CANDIDATE, ASSOCIATE, FOUNDER } + struct Associate { + address payable id; + uint share; + AssociateRole role; + } + + struct Proposal { + ProposalCategory category; + address owner; + Associate[] votes; + string descr; + } + + uint public minimumShare; + mapping(address => Associate) associates; + address[] associateIds; + address public immutable founder; + + Proposal[] public proposals; + + bool public override isDissoluted = false; + + function isAssociated(address id) external view override returns (bool) { + if (associates[id].id == address(0)) { + return false; + } + + return (associates[id].role == AssociateRole.ASSOCIATE || associates[id].role == AssociateRole.FOUNDER); + } + + + constructor(uint minimumAssociatingShare) payable { + assert(minimumAssociatingShare == msg.value); + minimumShare = msg.value; + associates[msg.sender] = Associate({ + id: payable(msg.sender), + share: msg.value, + role: AssociateRole.FOUNDER + }); + + associateIds.push(msg.sender); + founder = msg.sender; + } + + modifier NeedDissolution(bool action) { + if (!action) { + require(!isDissoluted, "Can't do this action because the corporate is dissoluting"); + } else { + require(isDissoluted, "Can't do this action because the corporate is not dissoluting"); + } + _; + } + + modifier NeedAssociate { + if (!this.isAssociated(msg.sender)) { + revert("You must be an associated to vote this proposal."); + } + _; + } + + function depositFunds() external override payable NeedDissolution(false) { + if (associates[msg.sender].id == address(0)) { + require(msg.value >= minimumShare, "The share must be at least equals to the minimum"); + associates[msg.sender] = Associate({ + id: payable(msg.sender), + share: msg.value, + role: AssociateRole.CANDIDATE + }); + Proposal storage p = proposals.push(); + p.category = ProposalCategory.NewAssociationAcceptance; + p.owner = msg.sender; + + emit NewAssociateCandidate(proposals.length-1, p.owner); + } else { + associates[msg.sender].share += msg.value; + } + } + + + function depositGenericProposal(string calldata description) external override NeedDissolution(false) NeedAssociate { + Proposal storage p = proposals.push(); + p.category = ProposalCategory.Generic; + p.owner = msg.sender; + p.descr = description; + + emit NewGenericProposal(proposals.length-1, description); + } + + + function depositDissolutionProposal() external override NeedDissolution(false) NeedAssociate { + Proposal storage p = proposals.push(); + p.category = ProposalCategory.CorporateDissolution; + p.owner = msg.sender; + + emit NewDissolutionProposal(proposals.length-1); + } + + + function voteProposal(uint proposalId) external override NeedDissolution(false) NeedAssociate { + Proposal storage p = proposals[proposalId]; + uint totalShare = 0; + for (uint i = 0; i < p.votes.length; ++i) { + totalShare += p.votes[i].share; + if (p.votes[i].id == msg.sender) { + revert("Already voted this proposal!"); + } + } + p.votes.push(associates[msg.sender]); + totalShare += associates[msg.sender].share; + + uint minQuote = (address(this).balance / 2) + 1; + + if (p.category == ProposalCategory.CorporateDissolution && p.votes.length == associateIds.length) { + isDissoluted = true; + emit AcceptedCorporateDissolution(); + } else if (totalShare >= minQuote) { + if (p.category == ProposalCategory.NewAssociationAcceptance) { + assert(associates[p.owner].role == AssociateRole.CANDIDATE); + associateIds.push(p.owner); + associates[p.owner].role = AssociateRole.ASSOCIATE; + emit AcceptedAssociate(p.owner); + } else if (p.category == ProposalCategory.Generic) { + emit AcceptedGenericProposal(p.descr); + } + } + } + + + function requestShareRefunding() external override NeedDissolution(true) { + if (associates[msg.sender].id == address(0)) { + revert("You're not one of the associates, can't refund"); + } + + associates[msg.sender].id.transfer(associates[msg.sender].share); + } +} diff --git a/Year_3/Blockchain/RandomMoney.sol b/Year_3/Blockchain/RandomMoney.sol new file mode 100644 index 0000000..2a5def4 --- /dev/null +++ b/Year_3/Blockchain/RandomMoney.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >0.7 <0.9; + +contract RandomMoney { + uint public total; + uint public constant minFee = 10; + mapping(address => uint) public amounts; + address[] private members; + + + function play() public payable returns (uint) { + require(msg.value >= minFee, "Value is less than the required minimum"); + + + amounts[msg.sender] += msg.value; + + if (amounts[msg.sender] == msg.value) { + members.push(msg.sender); + } + + total += msg.value; + + return total; + } + + function pay() public payable { + address payable account = payable(randomAddress()); + account.transfer(total); + total = 0; + } + + function randomAddress() internal view returns (address) { + uint8 num = uint8( + uint256( + keccak256(abi.encode(block.timestamp, block.difficulty)) + ) % members.length + ); + + return members[num]; + } +} + diff --git a/Year_3/Blockchain/RockPaperScissor.sol b/Year_3/Blockchain/RockPaperScissor.sol new file mode 100644 index 0000000..528bc4c --- /dev/null +++ b/Year_3/Blockchain/RockPaperScissor.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "TrustworthyRockPaperScissorsTournamentSpecs.sol"; + +contract TrustworthyRockPaperScissorsTournament is TrustworthyRockPaperScissorsTournamentSpecs { + enum Move { ROCK, PAPER, SCISSOR } + enum Status { IN_PROGRESS, END } + struct PlayerInfo { + address payable id; + uint8 wins; + Move[] games; + uint fees; + } + + mapping(Player => PlayerInfo) players; + uint8 target; + uint256 matchFee; + Status status; + Player winner; + address payable owner; + + uint8 override public disputedMatches; + + constructor( + address payable firstPlayer, + address payable secondPlayer, + uint8 targetWins, + uint256 singleMatchFee + ) { + require(firstPlayer != secondPlayer, "Players must be two different addresses."); + require(firstPlayer != address(0) && secondPlayer != address(0), "Players address are not valid."); + + players[Player.First].id = firstPlayer; + players[Player.Second].id = secondPlayer; + target = targetWins; + matchFee = singleMatchFee; + status = Status.IN_PROGRESS; + disputedMatches = 0; + owner = payable(msg.sender); + } + + function moveRock() external override payable { + move(Move.ROCK); + } + + function movePaper() external override payable { + move(Move.PAPER); + } + + function moveScissor() external override payable { + move(Move.SCISSOR); + } + + modifier mustBeAPlayer() { + require(msg.sender == players[Player.First].id || msg.sender == players[Player.Second].id, "You're not one of the players."); + _; + } + + function move(Move move_type) internal mustBeAPlayer { + require(status != Status.END, "Game has already finish"); + require(msg.value >= matchFee, "You need to pass the minimum fee to play"); + + Player p = (msg.sender == players[Player.First].id ? Player.First : Player.Second); + players[p].games.push(move_type); + players[p].fees += msg.value; + + checkGame(); + } + + function checkGame() internal { + uint8 n = uint8( + players[Player.First].games.length < players[Player.Second].games.length + ? players[Player.First].games.length + : players[Player.Second].games.length + ); + + for (uint8 i = disputedMatches; i < n; ++i) { + Move p1 = players[Player.First].games[i]; + Move p2 = players[Player.Second].games[i]; + + // Fair play + if (p1 == p2) continue; + + if ( + (p1 == Move.ROCK && p2 == Move.SCISSOR) || + (p1 == Move.SCISSOR && p2 == Move.PAPER) || + (p1 == Move.PAPER && p2 == Move.ROCK) + ) { + players[Player.First].wins++; + emit MatchWonBy(Player.First, i); + } else { + players[Player.Second].wins++; + emit MatchWonBy(Player.Second, i); + } + + disputedMatches++; + + if (players[Player.First].wins == target) { + emit TournamentWonBy(Player.First); + winner = Player.First; + endGame(); + } else if (players[Player.Second].wins == target) { + emit TournamentWonBy(Player.Second); + winner = Player.Second; + endGame(); + } + } + } + + function endGame() internal { + require(status != Status.END, "Game has already finish"); + + assert(players[winner].wins == target); + + uint total = players[Player.First].fees + players[Player.Second].fees; + assert(total == address(this).balance); + players[winner].id.transfer(total); + + status = Status.END; + selfdestruct(owner); + } +} diff --git a/Year_3/Blockchain/TicTacToe.sol b/Year_3/Blockchain/TicTacToe.sol new file mode 100644 index 0000000..582e10d --- /dev/null +++ b/Year_3/Blockchain/TicTacToe.sol @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: None + +pragma solidity ^0.8.0; + +contract TicTacToe { + enum Player { X, O } + struct PlayerInfo { + address payable id; + Player p_type; + } + enum Cell { X, O, NONE } + enum GameStatus { TURN_X, TURN_O, WAIT, END } + + mapping(address => PlayerInfo) players; + address[2] playerIds; + Cell[3][3] cells; + Player turn; + GameStatus status = GameStatus.WAIT; + uint immutable minFee; + uint fee; + uint8 moves = 9; + + event Movement(address sender, Player player, Cell[3][3] cells); + event EndGame(address winner, uint amount); + + constructor(address payable firstPlayer, address payable secondPlayer, uint _minFee) { + require(firstPlayer != address(0) && secondPlayer != address(0), "You must define both address"); + require(firstPlayer != secondPlayer, "Players must be different"); + + players[firstPlayer] = PlayerInfo({ + id: firstPlayer, + p_type: Player.X + }); + playerIds[0] = firstPlayer; + players[secondPlayer] = PlayerInfo({ + id: secondPlayer, + p_type: Player.O + }); + playerIds[1] = secondPlayer; + minFee = _minFee; + turn = Player(getRandomTurn()); + + for (uint8 i = 0; i < 3; ++i) + for (uint8 j = 0; j < 3; ++j) + cells[i][j] = Cell.NONE; + } + + function play(uint x, uint y) public payable { + require(status != GameStatus.END, "Game is over"); + require(players[msg.sender].id != address(0), "You're not one of the players"); + require(players[msg.sender].p_type == turn, "It's not your turn"); + require(msg.value >= minFee, "Value is less than the min fee"); + + fee += msg.value; + + if (cells[y][x] != Cell.NONE) { + revert("Can't use this cell"); + } + + cells[y][x] = (turn == Player.X ? Cell.X : Cell.O); + turn = Player(1 - uint(turn)); + moves--; + checkGame(); + } + + function getBoard() public view returns (Cell[3][3] memory) { + return cells; + } + + /* + ------------- + | | | | + |---|---|---| + | | | | + |---|---|---| + | | | | + ------------- + */ + + function checkGame() internal { + // It's useless to check when moves > 4 + if (moves > 4) return; + + bool result = false; + PlayerInfo memory winner; + + if (moves == 0) { + emit EndGame(address(this), fee); + status = GameStatus.END; + } + + // Diagonal wins + if ( + (cells[0][0] == cells[1][1] && cells[0][0] == cells[2][2]) || + (cells[0][2] == cells[1][1] && cells[0][2] == cells[2][0]) + ) { + address addr = address(0); + if (cells[0][0] != Cell.NONE) { + addr = playerIds[uint256(cells[0][0])]; + winner = players[addr]; + assert(uint(winner.p_type) == uint(cells[0][0])); + result = true; + } else if (cells[0][2] != Cell.NONE) { + addr = playerIds[uint256(cells[0][2])]; + winner = players[addr]; + assert(uint(winner.p_type) == uint(cells[0][2])); + result = true; + } + } + + if (!result) { + for (uint8 i = 0; i < 3; i+=3) { + for (uint8 j = 0; j < 3; j+=3) { + if (cells[i][j] == Cell.NONE) continue; + + address addr = playerIds[uint256(cells[i][j])]; + + if ( + (cells[i][j] == cells[i+1][j] && cells[i][j] == cells[i+2][j]) || + (cells[i][j] == cells[i][j+1] && cells[i][j+1] == cells[i][j+2]) + ) { + result = true; + } + + if (result) { + winner = players[addr]; + assert(uint(winner.p_type) == uint(cells[i][j])); + break; + } + + } + + if (result) break; + } + } + + if (result && winner.id != address(0)) { + assert(fee == address(this).balance); + winner.id.transfer(fee); + status = GameStatus.END; + emit EndGame(winner.id, fee); + fee = 0; + } + } + + function getRandomTurn() internal view returns (uint8) { + return uint8( + uint256( + keccak256(abi.encode(block.timestamp, block.difficulty)) + ) % 2 + ); + } +} -- cgit v1.2.3-18-g5258