├── .gitattributes ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .solhint.json ├── LICENSE ├── README.md ├── buidler.config.js ├── contracts ├── MultipleArbitrableTokenTransactionWithAppeals.sol ├── MultipleArbitrableTokenTransactionWithFee.sol ├── MultipleArbitrableTransactionWithAppeals.sol ├── MultipleArbitrableTransactionWithFee.sol ├── libraries │ └── CappedMath.sol └── mocks │ ├── ERC20Mock.sol │ └── TestArbitrator.sol ├── package.json ├── scripts └── deploy_test.js ├── src ├── entities │ ├── dispute-ruling.js │ ├── transaction-party.js │ └── transaction-status.js └── test-helpers.js ├── test ├── README.md ├── multiple-arbitrable-token-transaction-with-appeals.gas-cost.js ├── multiple-arbitrable-token-transaction-with-appeals.test.js ├── multiple-arbitrable-token-transaction-with-fee.test.js ├── multiple-arbitrable-transaction-with-appeals.gas-cost.js ├── multiple-arbitrable-transaction-with-appeals.test.js └── multiple-arbitrable-transaction-with-fee.test.js └── yarn.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #Buidler files 2 | cache 3 | artifacts 4 | 5 | # Created by https://www.toptal.com/developers/gitignore/api/vim,node,visualstudiocode,yarn 6 | # Edit at https://www.toptal.com/developers/gitignore?templates=vim,node,visualstudiocode,yarn 7 | 8 | ### Node ### 9 | # Logs 10 | logs 11 | *.log 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | lerna-debug.log* 16 | 17 | # Diagnostic reports (https://nodejs.org/api/report.html) 18 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 19 | 20 | # Runtime data 21 | pids 22 | *.pid 23 | *.seed 24 | *.pid.lock 25 | 26 | # Directory for instrumented libs generated by jscoverage/JSCover 27 | lib-cov 28 | 29 | # Coverage directory used by tools like istanbul 30 | coverage 31 | *.lcov 32 | 33 | # nyc test coverage 34 | .nyc_output 35 | 36 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 37 | .grunt 38 | 39 | # Bower dependency directory (https://bower.io/) 40 | bower_components 41 | 42 | # node-waf configuration 43 | .lock-wscript 44 | 45 | # Compiled binary addons (https://nodejs.org/api/addons.html) 46 | build/Release 47 | 48 | # Dependency directories 49 | node_modules/ 50 | jspm_packages/ 51 | 52 | # TypeScript v1 declaration files 53 | typings/ 54 | 55 | # TypeScript cache 56 | *.tsbuildinfo 57 | 58 | # Optional npm cache directory 59 | .npm 60 | 61 | # Optional eslint cache 62 | .eslintcache 63 | 64 | # Microbundle cache 65 | .rpt2_cache/ 66 | .rts2_cache_cjs/ 67 | .rts2_cache_es/ 68 | .rts2_cache_umd/ 69 | 70 | # Optional REPL history 71 | .node_repl_history 72 | 73 | # Output of 'npm pack' 74 | *.tgz 75 | 76 | # Yarn Integrity file 77 | .yarn-integrity 78 | 79 | # dotenv environment variables file 80 | .env 81 | .env.test 82 | 83 | # parcel-bundler cache (https://parceljs.org/) 84 | .cache 85 | 86 | # Next.js build output 87 | .next 88 | 89 | # Nuxt.js build / generate output 90 | .nuxt 91 | dist 92 | 93 | # Gatsby files 94 | .cache/ 95 | # Comment in the public line in if your project uses Gatsby and not Next.js 96 | # https://nextjs.org/blog/next-9-1#public-directory-support 97 | # public 98 | 99 | # vuepress build output 100 | .vuepress/dist 101 | 102 | # Serverless directories 103 | .serverless/ 104 | 105 | # FuseBox cache 106 | .fusebox/ 107 | 108 | # DynamoDB Local files 109 | .dynamodb/ 110 | 111 | # TernJS port file 112 | .tern-port 113 | 114 | # Stores VSCode versions used for testing VSCode extensions 115 | .vscode-test 116 | 117 | ### Vim ### 118 | # Swap 119 | [._]*.s[a-v][a-z] 120 | !*.svg # comment out if you don't need vector files 121 | [._]*.sw[a-p] 122 | [._]s[a-rt-v][a-z] 123 | [._]ss[a-gi-z] 124 | [._]sw[a-p] 125 | 126 | # Session 127 | Session.vim 128 | Sessionx.vim 129 | 130 | # Temporary 131 | .netrwhist 132 | *~ 133 | # Auto-generated tag files 134 | tags 135 | # Persistent undo 136 | [._]*.un~ 137 | 138 | ### VisualStudioCode ### 139 | .vscode/* 140 | !.vscode/settings.json 141 | !.vscode/tasks.json 142 | !.vscode/launch.json 143 | !.vscode/extensions.json 144 | *.code-workspace 145 | 146 | ### VisualStudioCode Patch ### 147 | # Ignore all local history of files 148 | .history 149 | 150 | ### yarn ### 151 | # https://yarnpkg.com/advanced/qa#which-files-should-be-gitignored 152 | 153 | # .yarn/unplugged and .yarn/build-state.yml should likely always be ignored since 154 | # they typically hold machine-specific build artifacts. Ignoring them might however 155 | # prevent Zero-Installs from working (to prevent this, set enableScripts to false). 156 | .yarn/unplugged 157 | .yarn/build-state.yml 158 | 159 | # .yarn/cache and .pnp.* may be safely ignored, but you'll need to run yarn install 160 | # to regenerate them between each branch switch. 161 | # Uncomment the following lines if you're not using Zero-Installs: 162 | # .yarn/cache 163 | # .pnp.* 164 | 165 | # End of https://www.toptal.com/developers/gitignore/api/vim,node,visualstudiocode,yarn 166 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | package-lock.json 3 | package-template.json 4 | .sourcemaps/ 5 | coverage/ 6 | platforms/ 7 | plugins/ 8 | www/ 9 | src/assets/*.json 10 | .next 11 | .* 12 | abi 13 | build 14 | types 15 | coverage.json 16 | cache 17 | artifacts 18 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "arrowParens": "avoid", 4 | "bracketSpacing": true, 5 | "endOfLine":"auto", 6 | "printWidth": 100, 7 | "singleQuote": false, 8 | "tabWidth": 2, 9 | "useTabs": false, 10 | "trailingComma": "all", 11 | "overrides": [ 12 | { 13 | "files": "*.sol", 14 | "options": { 15 | "tabWidth": 4 16 | } 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "code-complexity": ["error", 7], 6 | "compiler-version": ["error", ">=0.4.0 <0.9.0"], 7 | "const-name-snakecase": "off", 8 | "constructor-syntax": "error", 9 | "func-visibility": ["error", { "ignoreConstructors": true }], 10 | "max-line-length": ["error", 120], 11 | "prettier/prettier": [ 12 | "error", 13 | { 14 | "endOfLine": "auto" 15 | } 16 | ], 17 | "reason-string": ["warn", { "maxLength": 64 }], 18 | "not-rely-on-time": "off", 19 | "check-send-result": "off" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Kleros 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Escrow Contracts 2 | 3 | ## Deployed Addresses 4 | 5 | - Kovan: `` 6 | - Mainnet: `` 7 | 8 | ## Contributing 9 | 10 | ### Install Dependencies 11 | 12 | ```bash 13 | yarn install 14 | ``` 15 | 16 | ### Run Tests 17 | 18 | ```bash 19 | yarn test 20 | ``` 21 | 22 | ### Compile the Contracts 23 | 24 | ```bash 25 | yarn build 26 | ``` 27 | 28 | ### Run Linter on Files 29 | 30 | ```bash 31 | yarn lint 32 | ``` 33 | 34 | ### Fix Linter Issues on Files 35 | 36 | ```bash 37 | yarn fix 38 | ``` 39 | -------------------------------------------------------------------------------- /buidler.config.js: -------------------------------------------------------------------------------- 1 | const { usePlugin, task } = require("@nomiclabs/buidler/config"); 2 | 3 | usePlugin("@nomiclabs/buidler-waffle"); 4 | usePlugin("@nomiclabs/buidler-ethers"); 5 | usePlugin("@nomiclabs/buidler-web3"); 6 | 7 | // Set the following variables if you want to deploy the contracts for testing using goerli 8 | const GOERLI_PRIVATE_KEY = ""; 9 | const GOERLI_INFURA_PROJECT_ID = ""; 10 | 11 | // This is a sample Buidler task. To learn how to create your own go to 12 | // https://buidler.dev/guides/create-task.html 13 | task("accounts", "Prints the list of accounts", async (_, { ethers }) => { 14 | const accounts = await ethers.getSigners(); 15 | 16 | for (const account of accounts) console.log(await account.getAddress()); 17 | }); 18 | 19 | // You have to export an object to set up your config 20 | // This object can have the following optional entries: 21 | // defaultNetwork, networks, solc, and paths. 22 | // Go to https://buidler.dev/config/ to learn more 23 | module.exports = { 24 | // This is a sample solc configuration that specifies which version of solc to use 25 | solc: { 26 | version: "0.8.9", 27 | optimizer: { 28 | enabled: true, 29 | runs: 200, 30 | }, 31 | }, 32 | paths: { 33 | sources: "contracts", 34 | cache: "cache", 35 | artifacts: "artifacts", 36 | }, 37 | networks: { 38 | goerli: { 39 | url: `https://goerli.infura.io/v3/${GOERLI_INFURA_PROJECT_ID}`, 40 | accounts: [`0x${GOERLI_PRIVATE_KEY}`], 41 | }, 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /contracts/MultipleArbitrableTokenTransactionWithAppeals.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | /** 4 | * @authors: [@unknownunknown1, @fnanni-0, @shalzz] 5 | * @reviewers: [@ferittuncer*, @epiqueras*, @nix1g*, @unknownunknown1, @alcercu*, @fnanni-0*] 6 | * @auditors: [] 7 | * @bounties: [] 8 | */ 9 | 10 | pragma solidity 0.8.9; 11 | 12 | import "@kleros/erc-792/contracts/IArbitrable.sol"; 13 | import "@kleros/erc-792/contracts/IArbitrator.sol"; 14 | import "@kleros/erc-792/contracts/erc-1497/IEvidence.sol"; 15 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 16 | import "./libraries/CappedMath.sol"; 17 | 18 | /** @title Multiple Arbitrable ERC20 Token Transaction 19 | * This is a contract for multiple arbitrated token transactions which can 20 | * be reversed by an arbitrator. 21 | * This can be used for buying goods, services and for paying freelancers. 22 | * Parties are identified as "sender" and "receiver". 23 | * This version of the contract supports appeal crowdfunding. 24 | * Note that the contract expects the tokens to have standard ERC20 behaviour. 25 | * The tokens that don't conform to this type of behaviour should be filtered by the UI. 26 | * Tokens should not reenter or allow recipients to refuse the transfer. 27 | * Also note that for ETH send() function is used deliberately instead of transfer() 28 | * to avoid blocking the flow with reverting fallback. 29 | */ 30 | contract MultipleArbitrableTokenTransactionWithAppeals is IArbitrable, IEvidence { 31 | using CappedMath for uint256; 32 | 33 | // **************************** // 34 | // * Contract variables * // 35 | // **************************** // 36 | 37 | uint256 public constant AMOUNT_OF_CHOICES = 2; 38 | uint256 public constant MULTIPLIER_DIVISOR = 10000; // Divisor parameter for multipliers. 39 | 40 | enum Party { 41 | None, 42 | Sender, 43 | Receiver 44 | } 45 | enum Status { 46 | NoDispute, 47 | WaitingSettlementSender, 48 | WaitingSettlementReceiver, 49 | WaitingSender, 50 | WaitingReceiver, 51 | DisputeCreated, 52 | Resolved 53 | } 54 | enum Resolution { 55 | TransactionExecuted, 56 | TimeoutBySender, 57 | TimeoutByReceiver, 58 | RulingEnforced, 59 | SettlementReached 60 | } 61 | 62 | struct Transaction { 63 | address payable sender; 64 | address payable receiver; 65 | uint256 amount; 66 | uint256 settlementSender; // Settlement amount proposed by the sender 67 | uint256 settlementReceiver; // Settlement amount proposed by the receiver 68 | IERC20 token; 69 | uint256 deadline; // Timestamp at which the transaction can be automatically executed if not disputed. 70 | uint256 disputeID; // If dispute exists, the ID of the dispute. 71 | uint256 senderFee; // Total fees paid by the sender. 72 | uint256 receiverFee; // Total fees paid by the receiver. 73 | uint256 lastInteraction; // Last interaction for the dispute procedure. 74 | Status status; 75 | } 76 | 77 | struct Round { 78 | uint256[3] paidFees; // Tracks the fees paid by each side in this round. 79 | // If the round is appealed, i.e. this is not the last round, 80 | // Party.None means that both sides have paid. 81 | Party sideFunded; 82 | // Sum of reimbursable fees and stake rewards available to the parties 83 | // that made contributions to the side that ultimately wins a dispute. 84 | uint256 feeRewards; 85 | mapping(address => uint256[3]) contributions; // Maps contributors to their contributions for each side. 86 | } 87 | 88 | /** 89 | * @dev Tracks the state of eventual disputes. 90 | */ 91 | struct TransactionDispute { 92 | uint256 transactionID; // The transaction ID. 93 | bool hasRuling; // Required to differentiate between having no ruling and a RefusedToRule ruling. 94 | Party ruling; // The ruling given by the arbitrator. 95 | } 96 | 97 | IArbitrator public immutable arbitrator; // Address of the arbitrator contract. TRUSTED. 98 | bytes public arbitratorExtraData; // Extra data to set up the arbitration. 99 | // Time in seconds a party can take to pay arbitration fees before being 100 | // considered unresponsive and lose the dispute. 101 | uint256 public immutable feeTimeout; 102 | 103 | // Time in seconds a party can take to accept or propose a settlement 104 | // before being considered unresponsive and the case can be arbitrated. 105 | uint256 public immutable settlementTimeout; 106 | 107 | // Multiplier for calculating the appeal fee that must be paid by the 108 | // submitter in the case where there is no winner or loser 109 | // (e.g. when the arbitrator ruled "refuse to arbitrate"). 110 | uint256 public immutable sharedStakeMultiplier; 111 | // Multiplier for calculating the appeal fee of the party that won the previous round. 112 | uint256 public immutable winnerStakeMultiplier; 113 | // Multiplier for calculating the appeal fee of the party that lost the previous round. 114 | uint256 public immutable loserStakeMultiplier; 115 | 116 | /// @dev Stores the hashes of all transactions. 117 | bytes32[] public transactionHashes; 118 | 119 | /// @dev Maps a transactionID to its respective appeal rounds. 120 | mapping(uint256 => Round[]) public roundsByTransactionID; 121 | 122 | /// @dev Maps a disputeID to its respective transaction dispute. 123 | mapping(uint256 => TransactionDispute) public disputeIDtoTransactionDispute; 124 | 125 | // **************************** // 126 | // * Events * // 127 | // **************************** // 128 | 129 | /** 130 | * @dev To be emitted whenever a transaction state is updated. 131 | * @param _transactionID The ID of the changed transaction. 132 | * @param _transaction The full transaction data after update. 133 | */ 134 | event TransactionStateUpdated(uint256 indexed _transactionID, Transaction _transaction); 135 | 136 | /** @dev To be emitted when a party pays or reimburses the other. 137 | * @param _transactionID The index of the transaction. 138 | * @param _amount The amount paid. 139 | * @param _party The party that paid. 140 | */ 141 | event Payment(uint256 indexed _transactionID, uint256 _amount, address _party); 142 | 143 | /** @dev Indicate that a party has to pay a fee or would otherwise be considered as losing. 144 | * @param _transactionID The index of the transaction. 145 | * @param _party The party who has to pay. 146 | */ 147 | event HasToPayFee(uint256 indexed _transactionID, Party _party); 148 | 149 | /** @dev Emitted when a transaction is created. 150 | * @param _transactionID The index of the transaction. 151 | * @param _sender The address of the sender. 152 | * @param _receiver The address of the receiver. 153 | * @param _token The token address. 154 | * @param _amount The initial amount in the transaction. 155 | */ 156 | event TransactionCreated( 157 | uint256 indexed _transactionID, 158 | address indexed _sender, 159 | address indexed _receiver, 160 | IERC20 _token, 161 | uint256 _amount 162 | ); 163 | 164 | /** @dev To be emitted when a transaction is resolved, either by its execution, 165 | * a timeout or because a ruling was enforced. 166 | * @param _transactionID The ID of the respective transaction. 167 | * @param _resolution Short description of what caused the transaction to be solved. 168 | */ 169 | event TransactionResolved(uint256 indexed _transactionID, Resolution indexed _resolution); 170 | 171 | /** @dev To be emitted when the appeal fees of one of the parties are fully funded. 172 | * @param _transactionID The ID of the respective transaction. 173 | * @param _party The party that is fully funded. 174 | */ 175 | event HasPaidAppealFee(uint256 indexed _transactionID, Party _party); 176 | 177 | /** 178 | * @dev To be emitted when someone contributes to the appeal process. 179 | * @param _transactionID The ID of the respective transaction. 180 | * @param _party The party which received the contribution. 181 | * @param _contributor The address of the contributor. 182 | * @param _amount The amount contributed. 183 | */ 184 | event AppealContribution( 185 | uint256 indexed _transactionID, 186 | Party _party, 187 | address _contributor, 188 | uint256 _amount 189 | ); 190 | 191 | // **************************** // 192 | // * Arbitrable functions * // 193 | // * Modifying the state * // 194 | // **************************** // 195 | 196 | /** @dev Constructor. 197 | * @param _arbitrator The arbitrator of the contract. 198 | * @param _arbitratorExtraData Extra data for the arbitrator. 199 | * @param _feeTimeout Arbitration fee timeout for the parties. 200 | * @param _settlementTimeout Settlement timeout for the parties. 201 | * @param _sharedStakeMultiplier Multiplier of the appeal cost that the 202 | * submitter must pay for a round when there is no winner/loser in 203 | * the previous round. In basis points. 204 | * @param _winnerStakeMultiplier Multiplier of the appeal cost that the winner 205 | * has to pay for a round. In basis points. 206 | * @param _loserStakeMultiplier Multiplier of the appeal cost that the loser 207 | * has to pay for a round. In basis points. 208 | */ 209 | constructor( 210 | IArbitrator _arbitrator, 211 | bytes memory _arbitratorExtraData, 212 | uint256 _feeTimeout, 213 | uint256 _settlementTimeout, 214 | uint256 _sharedStakeMultiplier, 215 | uint256 _winnerStakeMultiplier, 216 | uint256 _loserStakeMultiplier 217 | ) { 218 | arbitrator = _arbitrator; 219 | arbitratorExtraData = _arbitratorExtraData; 220 | feeTimeout = _feeTimeout; 221 | settlementTimeout = _settlementTimeout; 222 | sharedStakeMultiplier = _sharedStakeMultiplier; 223 | winnerStakeMultiplier = _winnerStakeMultiplier; 224 | loserStakeMultiplier = _loserStakeMultiplier; 225 | } 226 | 227 | modifier onlyValidTransaction(uint256 _transactionID, Transaction memory _transaction) { 228 | require( 229 | transactionHashes[_transactionID - 1] == hashTransactionState(_transaction), 230 | "Transaction doesn't match stored hash." 231 | ); 232 | _; 233 | } 234 | 235 | /// @dev Using calldata as data location makes gas consumption more efficient 236 | /// when caller function also uses calldata. 237 | modifier onlyValidTransactionCD(uint256 _transactionID, Transaction calldata _transaction) { 238 | require( 239 | transactionHashes[_transactionID - 1] == hashTransactionStateCD(_transaction), 240 | "Transaction doesn't match stored hash." 241 | ); 242 | _; 243 | } 244 | 245 | /** @dev Create a transaction. UNTRUSTED. 246 | * @param _amount The amount of tokens in this transaction. 247 | * @param _token The ERC20 token contract. 248 | * @param _timeoutPayment Time after which a party automatically loses a dispute. 249 | * @param _receiver The recipient of the transaction. 250 | * @param _metaEvidence Link to the meta-evidence. 251 | * @return transactionID The index of the transaction. 252 | */ 253 | function createTransaction( 254 | uint256 _amount, 255 | IERC20 _token, 256 | uint256 _timeoutPayment, 257 | address payable _receiver, 258 | string calldata _metaEvidence 259 | ) external returns (uint256 transactionID) { 260 | // Transfers token from sender wallet to contract. 261 | require( 262 | _token.transferFrom(msg.sender, address(this), _amount), 263 | "Sender does not have enough approved funds." 264 | ); 265 | 266 | Transaction memory transaction; 267 | transaction.sender = payable(msg.sender); 268 | transaction.receiver = _receiver; 269 | transaction.amount = _amount; 270 | transaction.token = _token; 271 | transaction.deadline = block.timestamp.addCap(_timeoutPayment); 272 | 273 | transactionHashes.push(hashTransactionState(transaction)); 274 | // transactionID starts at 1. This way, TransactionDispute can check if 275 | // a dispute exists by testing transactionID != 0. 276 | transactionID = transactionHashes.length; 277 | 278 | emit TransactionCreated(transactionID, msg.sender, _receiver, _token, _amount); 279 | emit TransactionStateUpdated(transactionID, transaction); 280 | emit MetaEvidence(transactionID, _metaEvidence); 281 | } 282 | 283 | /** @notice Pay receiver. To be called if the good or service is provided. 284 | * Can only be called by the sender. 285 | * @dev UNTRUSTED 286 | * @param _transactionID The index of the transaction. 287 | * @param _transaction The transaction state. 288 | * @param _amount Amount to pay in wei. 289 | */ 290 | function pay( 291 | uint256 _transactionID, 292 | Transaction memory _transaction, 293 | uint256 _amount 294 | ) external onlyValidTransaction(_transactionID, _transaction) { 295 | require(_transaction.sender == msg.sender, "The caller must be the sender."); 296 | require(_transaction.status == Status.NoDispute, "The transaction must not be disputed."); 297 | require(_amount <= _transaction.amount, "Maximum amount available for payment exceeded."); 298 | 299 | _transaction.amount -= _amount; 300 | transactionHashes[_transactionID - 1] = hashTransactionState(_transaction); 301 | 302 | require( 303 | _transaction.token.transfer(_transaction.receiver, _amount), 304 | "The `transfer` function must not fail." 305 | ); 306 | emit Payment(_transactionID, _amount, msg.sender); 307 | emit TransactionStateUpdated(_transactionID, _transaction); 308 | } 309 | 310 | /** @notice Reimburse sender. To be called if the good or service can't be fully provided. 311 | * Can only be called by the receiver. 312 | * @dev UNTRUSTED 313 | * @param _transactionID The index of the transaction. 314 | * @param _transaction The transaction state. 315 | * @param _amountReimbursed Amount to reimburse in wei. 316 | */ 317 | function reimburse( 318 | uint256 _transactionID, 319 | Transaction memory _transaction, 320 | uint256 _amountReimbursed 321 | ) external onlyValidTransaction(_transactionID, _transaction) { 322 | require(_transaction.receiver == msg.sender, "The caller must be the receiver."); 323 | require(_transaction.status == Status.NoDispute, "The transaction must not be disputed."); 324 | require( 325 | _amountReimbursed <= _transaction.amount, 326 | "Maximum reimbursement available exceeded." 327 | ); 328 | 329 | _transaction.amount -= _amountReimbursed; 330 | transactionHashes[_transactionID - 1] = hashTransactionState(_transaction); 331 | 332 | require( 333 | _transaction.token.transfer(_transaction.sender, _amountReimbursed), 334 | "The `transfer` function must not fail." 335 | ); 336 | emit Payment(_transactionID, _amountReimbursed, msg.sender); 337 | emit TransactionStateUpdated(_transactionID, _transaction); 338 | } 339 | 340 | /** @dev Transfer the transaction's amount to the receiver if the timeout has passed. UNTRUSTED 341 | * @param _transactionID The index of the transaction. 342 | * @param _transaction The transaction state. 343 | */ 344 | function executeTransaction(uint256 _transactionID, Transaction memory _transaction) 345 | external 346 | onlyValidTransaction(_transactionID, _transaction) 347 | { 348 | require(block.timestamp >= _transaction.deadline, "Deadline not passed."); 349 | require(_transaction.status == Status.NoDispute, "The transaction must not be disputed."); 350 | 351 | uint256 amount = _transaction.amount; 352 | _transaction.amount = 0; 353 | _transaction.status = Status.Resolved; 354 | transactionHashes[_transactionID - 1] = hashTransactionState(_transaction); 355 | 356 | require( 357 | _transaction.token.transfer(_transaction.receiver, amount), 358 | "The `transfer` function must not fail." 359 | ); 360 | 361 | emit TransactionStateUpdated(_transactionID, _transaction); 362 | emit TransactionResolved(_transactionID, Resolution.TransactionExecuted); 363 | } 364 | 365 | /** @notice Propose a settlement as a compromise from the initial terms to the other party. 366 | * @dev A party can only propose a settlement again after the other party has 367 | * done so as well to prevent front running/griefing issues. 368 | * @param _transactionID The index of the transaction. 369 | * @param _transaction The transaction state. 370 | * @param _amount The settlement amount. 371 | */ 372 | function proposeSettlement( 373 | uint256 _transactionID, 374 | Transaction memory _transaction, 375 | uint256 _amount 376 | ) external onlyValidTransaction(_transactionID, _transaction) { 377 | require( 378 | block.timestamp < _transaction.deadline || _transaction.status != Status.NoDispute, 379 | "Transaction expired" 380 | ); 381 | require( 382 | _transaction.status < Status.WaitingSender, 383 | "Transaction already escalated for arbitration" 384 | ); 385 | 386 | require( 387 | _amount <= _transaction.amount, 388 | "Settlement amount cannot be more that the initial amount" 389 | ); 390 | 391 | if (_transaction.status == Status.WaitingSettlementSender) { 392 | require(msg.sender == _transaction.sender, "The caller must be the sender."); 393 | _transaction.settlementSender = _amount; 394 | _transaction.status = Status.WaitingSettlementReceiver; 395 | } else if (_transaction.status == Status.WaitingSettlementReceiver) { 396 | require(msg.sender == _transaction.receiver, "The caller must be the receiver."); 397 | _transaction.settlementReceiver = _amount; 398 | _transaction.status = Status.WaitingSettlementSender; 399 | } else { 400 | if (msg.sender == _transaction.sender) { 401 | _transaction.settlementSender = _amount; 402 | _transaction.status = Status.WaitingSettlementReceiver; 403 | } else if (msg.sender == _transaction.receiver) { 404 | _transaction.settlementReceiver = _amount; 405 | _transaction.status = Status.WaitingSettlementSender; 406 | } else revert("Only the sender or receiver addresses are authorized"); 407 | } 408 | 409 | _transaction.lastInteraction = block.timestamp; 410 | transactionHashes[_transactionID - 1] = hashTransactionState(_transaction); 411 | emit TransactionStateUpdated(_transactionID, _transaction); 412 | } 413 | 414 | /** @notice Accept a settlement proposed by the other party. 415 | * @param _transactionID The index of the transaction. 416 | * @param _transaction The transaction state. 417 | */ 418 | function acceptSettlement(uint256 _transactionID, Transaction memory _transaction) 419 | external 420 | onlyValidTransaction(_transactionID, _transaction) 421 | { 422 | uint256 settlementAmount; 423 | if (_transaction.status == Status.WaitingSettlementSender) { 424 | require(msg.sender == _transaction.sender, "The caller must be the sender."); 425 | settlementAmount = _transaction.settlementReceiver; 426 | } else if (_transaction.status == Status.WaitingSettlementReceiver) { 427 | require(msg.sender == _transaction.receiver, "The caller must be the receiver."); 428 | settlementAmount = _transaction.settlementSender; 429 | } else revert("No settlement proposed to accept or tx already disputed/resolved."); 430 | 431 | uint256 remainingAmount = _transaction.amount - settlementAmount; 432 | 433 | _transaction.amount = 0; 434 | _transaction.settlementSender = 0; 435 | _transaction.settlementReceiver = 0; 436 | 437 | _transaction.status = Status.Resolved; 438 | transactionHashes[_transactionID - 1] = hashTransactionState(_transaction); // solhint-disable-line 439 | 440 | require( 441 | _transaction.token.transfer(_transaction.sender, remainingAmount), 442 | "The `transfer` function must not fail." 443 | ); 444 | require( 445 | _transaction.token.transfer(_transaction.receiver, settlementAmount), 446 | "The `transfer` function must not fail." 447 | ); 448 | 449 | emit TransactionStateUpdated(_transactionID, _transaction); 450 | emit TransactionResolved(_transactionID, Resolution.SettlementReached); 451 | } 452 | 453 | /** @dev Pay the arbitration fee to raise a dispute. To be called by the sender. UNTRUSTED. 454 | * Note that the arbitrator can have createDispute throw, which will make 455 | * this function throw and therefore lead to a party being timed-out. 456 | * This is not a vulnerability as the arbitrator can rule in favor of one party anyway. 457 | * @param _transactionID The index of the transaction. 458 | * @param _transaction The transaction state. 459 | */ 460 | function payArbitrationFeeBySender(uint256 _transactionID, Transaction memory _transaction) 461 | external 462 | payable 463 | onlyValidTransaction(_transactionID, _transaction) 464 | { 465 | require( 466 | _transaction.status == Status.WaitingSettlementSender || 467 | _transaction.status == Status.WaitingSettlementReceiver || 468 | _transaction.status == Status.WaitingSender, 469 | "Settlement not attempted first or the transaction already executed/disputed." 470 | ); 471 | 472 | // Allow the other party enough time to respond to a settlement before 473 | // allowing the proposer to raise a dispute. 474 | if (_transaction.status == Status.WaitingSettlementReceiver) { 475 | require( 476 | block.timestamp - _transaction.lastInteraction >= settlementTimeout, 477 | "Settlement period has not timed out yet." 478 | ); 479 | } 480 | 481 | require(msg.sender == _transaction.sender, "The caller must be the sender."); 482 | 483 | uint256 arbitrationCost = arbitrator.arbitrationCost(arbitratorExtraData); 484 | _transaction.senderFee += msg.value; 485 | // Require that the total paid to be at least the arbitration cost. 486 | require( 487 | _transaction.senderFee >= arbitrationCost, 488 | "The sender fee must cover arbitration costs." 489 | ); 490 | 491 | _transaction.lastInteraction = block.timestamp; 492 | // The receiver still has to pay. This can also happen if he has paid, but `arbitrationCost` has increased. 493 | if (_transaction.receiverFee < arbitrationCost) { 494 | _transaction.status = Status.WaitingReceiver; 495 | emit HasToPayFee(_transactionID, Party.Receiver); 496 | } else { 497 | // The receiver has also paid the fee. We create the dispute. 498 | raiseDispute(_transactionID, _transaction, arbitrationCost); 499 | } 500 | 501 | transactionHashes[_transactionID - 1] = hashTransactionState(_transaction); 502 | emit TransactionStateUpdated(_transactionID, _transaction); 503 | } 504 | 505 | /** @dev Pay the arbitration fee to raise a dispute. To be called by the receiver. UNTRUSTED. 506 | * Note that this function mirrors payArbitrationFeeBySender. 507 | * @param _transactionID The index of the transaction. 508 | * @param _transaction The transaction state. 509 | */ 510 | function payArbitrationFeeByReceiver(uint256 _transactionID, Transaction memory _transaction) 511 | external 512 | payable 513 | onlyValidTransaction(_transactionID, _transaction) 514 | { 515 | require( 516 | _transaction.status == Status.WaitingSettlementSender || 517 | _transaction.status == Status.WaitingSettlementReceiver || 518 | _transaction.status == Status.WaitingReceiver, 519 | "Settlement not attempted first or the transaction already executed/disputed." 520 | ); 521 | 522 | // Allow the other party enough time to respond to a settlement before 523 | // allowing the proposer to raise a dispute. 524 | if (_transaction.status == Status.WaitingSettlementSender) { 525 | require( 526 | block.timestamp - _transaction.lastInteraction >= settlementTimeout, 527 | "Settlement period has not timed out yet." 528 | ); 529 | } 530 | 531 | require(msg.sender == _transaction.receiver, "The caller must be the receiver."); 532 | 533 | uint256 arbitrationCost = arbitrator.arbitrationCost(arbitratorExtraData); 534 | _transaction.receiverFee += msg.value; 535 | // Require that the total paid to be at least the arbitration cost. 536 | require( 537 | _transaction.receiverFee >= arbitrationCost, 538 | "The receiver fee must cover arbitration costs." 539 | ); 540 | 541 | _transaction.lastInteraction = block.timestamp; 542 | // The sender still has to pay. This can also happen if he has paid, but arbitrationCost has increased. 543 | if (_transaction.senderFee < arbitrationCost) { 544 | _transaction.status = Status.WaitingSender; 545 | emit HasToPayFee(_transactionID, Party.Sender); 546 | } else { 547 | // The sender has also paid the fee. We create the dispute. 548 | raiseDispute(_transactionID, _transaction, arbitrationCost); 549 | } 550 | 551 | transactionHashes[_transactionID - 1] = hashTransactionState(_transaction); 552 | emit TransactionStateUpdated(_transactionID, _transaction); 553 | } 554 | 555 | /** @dev Reimburse sender if receiver fails to pay the fee. UNTRUSTED 556 | * @param _transactionID The index of the transaction. 557 | * @param _transaction The transaction state. 558 | */ 559 | function timeOutBySender(uint256 _transactionID, Transaction memory _transaction) 560 | external 561 | onlyValidTransaction(_transactionID, _transaction) 562 | { 563 | require( 564 | _transaction.status == Status.WaitingReceiver, 565 | "The transaction is not waiting on the receiver." 566 | ); 567 | require( 568 | block.timestamp - _transaction.lastInteraction >= feeTimeout, 569 | "Timeout time has not passed yet." 570 | ); 571 | 572 | if (_transaction.receiverFee != 0) { 573 | _transaction.receiver.send(_transaction.receiverFee); // It is the user responsibility to accept ETH. 574 | _transaction.receiverFee = 0; 575 | } 576 | 577 | uint256 amount = _transaction.amount; 578 | uint256 senderFee = _transaction.senderFee; 579 | 580 | _transaction.amount = 0; 581 | _transaction.settlementSender = 0; 582 | _transaction.settlementReceiver = 0; 583 | _transaction.senderFee = 0; 584 | _transaction.status = Status.Resolved; 585 | transactionHashes[_transactionID - 1] = hashTransactionState(_transaction); // solhint-disable-line 586 | 587 | require( 588 | _transaction.token.transfer(_transaction.sender, amount), 589 | "The `transfer` function must not fail." 590 | ); 591 | _transaction.sender.send(senderFee); // It is the user responsibility to accept ETH. 592 | emit TransactionStateUpdated(_transactionID, _transaction); 593 | emit TransactionResolved(_transactionID, Resolution.TimeoutBySender); 594 | } 595 | 596 | /** @dev Pay receiver if sender fails to pay the fee. UNTRUSTED 597 | * @param _transactionID The index of the transaction. 598 | * @param _transaction The transaction state. 599 | */ 600 | function timeOutByReceiver(uint256 _transactionID, Transaction memory _transaction) 601 | external 602 | onlyValidTransaction(_transactionID, _transaction) 603 | { 604 | require( 605 | _transaction.status == Status.WaitingSender, 606 | "The transaction is not waiting on the sender." 607 | ); 608 | require( 609 | block.timestamp - _transaction.lastInteraction >= feeTimeout, 610 | "Timeout time has not passed yet." 611 | ); 612 | 613 | if (_transaction.senderFee != 0) { 614 | _transaction.sender.send(_transaction.senderFee); // It is the user responsibility to accept ETH. 615 | _transaction.senderFee = 0; 616 | } 617 | 618 | uint256 amount = _transaction.amount; 619 | uint256 receiverFee = _transaction.receiverFee; 620 | 621 | _transaction.amount = 0; 622 | _transaction.settlementSender = 0; 623 | _transaction.settlementReceiver = 0; 624 | _transaction.receiverFee = 0; 625 | _transaction.status = Status.Resolved; 626 | transactionHashes[_transactionID - 1] = hashTransactionState(_transaction); // solhint-disable-line 627 | 628 | require( 629 | _transaction.token.transfer(_transaction.receiver, amount), 630 | "The `transfer` function must not fail." 631 | ); 632 | _transaction.receiver.send(receiverFee); // It is the user responsibility to accept ETH. 633 | 634 | emit TransactionStateUpdated(_transactionID, _transaction); 635 | emit TransactionResolved(_transactionID, Resolution.TimeoutByReceiver); 636 | } 637 | 638 | /** @dev Create a dispute. UNTRUSTED. 639 | * This function is internal and thus the transaction state validity is not checked. 640 | * Caller functions MUST do the check before calling this function. 641 | * _transaction MUST be a reference (not a copy) because its state is modified. 642 | * Caller functions MUST emit the TransactionStateUpdated event and update the hash. 643 | * @param _transactionID The index of the transaction. 644 | * @param _transaction The transaction state. 645 | * @param _arbitrationCost Amount to pay the arbitrator. 646 | */ 647 | function raiseDispute( 648 | uint256 _transactionID, 649 | Transaction memory _transaction, 650 | uint256 _arbitrationCost 651 | ) internal { 652 | _transaction.status = Status.DisputeCreated; 653 | _transaction.disputeID = arbitrator.createDispute{ value: _arbitrationCost }( 654 | AMOUNT_OF_CHOICES, 655 | arbitratorExtraData 656 | ); 657 | roundsByTransactionID[_transactionID].push(); 658 | TransactionDispute storage transactionDispute = disputeIDtoTransactionDispute[ 659 | _transaction.disputeID 660 | ]; 661 | transactionDispute.transactionID = _transactionID; 662 | emit Dispute(arbitrator, _transaction.disputeID, _transactionID, _transactionID); 663 | 664 | // Refund sender if it overpaid. 665 | if (_transaction.senderFee > _arbitrationCost) { 666 | uint256 extraFeeSender = _transaction.senderFee - _arbitrationCost; 667 | _transaction.senderFee = _arbitrationCost; 668 | _transaction.sender.send(extraFeeSender); // It is the user responsibility to accept ETH. 669 | } 670 | 671 | // Refund receiver if it overpaid. 672 | if (_transaction.receiverFee > _arbitrationCost) { 673 | uint256 extraFeeReceiver = _transaction.receiverFee - _arbitrationCost; 674 | _transaction.receiverFee = _arbitrationCost; 675 | _transaction.receiver.send(extraFeeReceiver); // It is the user responsibility to accept ETH. 676 | } 677 | } 678 | 679 | /** @dev Submit a reference to evidence. EVENT. 680 | * @param _transactionID The index of the transaction. 681 | * @param _transaction The transaction state. 682 | * @param _evidence A link to an evidence using its URI. 683 | */ 684 | function submitEvidence( 685 | uint256 _transactionID, 686 | Transaction calldata _transaction, 687 | string calldata _evidence 688 | ) external onlyValidTransactionCD(_transactionID, _transaction) { 689 | require( 690 | _transaction.status < Status.Resolved, 691 | "Must not send evidence if the dispute is resolved." 692 | ); 693 | 694 | emit Evidence(arbitrator, _transactionID, msg.sender, _evidence); 695 | } 696 | 697 | /** @dev Takes up to the total amount required to fund a side of an appeal. 698 | * Reimburses the rest. Creates an appeal if both sides are fully funded. 699 | * @param _transactionID The ID of the disputed transaction. 700 | * @param _transaction The transaction state. 701 | * @param _side The party that pays the appeal fee. 702 | */ 703 | function fundAppeal( 704 | uint256 _transactionID, 705 | Transaction calldata _transaction, 706 | Party _side 707 | ) external payable onlyValidTransactionCD(_transactionID, _transaction) { 708 | require(_side != Party.None, "Wrong party."); 709 | require(_transaction.status == Status.DisputeCreated, "No dispute to appeal"); 710 | 711 | (uint256 appealPeriodStart, uint256 appealPeriodEnd) = arbitrator.appealPeriod( 712 | _transaction.disputeID 713 | ); 714 | require( 715 | block.timestamp >= appealPeriodStart && block.timestamp < appealPeriodEnd, 716 | "Funding must be made within the appeal period." 717 | ); 718 | 719 | uint256 multiplier; 720 | uint256 winner = arbitrator.currentRuling(_transaction.disputeID); 721 | if (winner == uint256(_side)) { 722 | multiplier = winnerStakeMultiplier; 723 | } else if (winner == 0) { 724 | multiplier = sharedStakeMultiplier; 725 | } else { 726 | require( 727 | block.timestamp < (appealPeriodEnd + appealPeriodStart) / 2, 728 | "The loser must pay during the first half of the appeal period." 729 | ); 730 | multiplier = loserStakeMultiplier; 731 | } 732 | 733 | Round storage round = roundsByTransactionID[_transactionID][ 734 | roundsByTransactionID[_transactionID].length - 1 735 | ]; 736 | require(_side != round.sideFunded, "Appeal fee has already been paid."); 737 | 738 | uint256 appealCost = arbitrator.appealCost(_transaction.disputeID, arbitratorExtraData); 739 | uint256 totalCost = appealCost.addCap((appealCost.mulCap(multiplier)) / MULTIPLIER_DIVISOR); 740 | 741 | // Take up to the amount necessary to fund the current round at the current costs. 742 | uint256 contribution; // Amount contributed. 743 | uint256 remainingETH; // Remaining ETH to send back. 744 | (contribution, remainingETH) = calculateContribution( 745 | msg.value, 746 | totalCost.subCap(round.paidFees[uint256(_side)]) 747 | ); 748 | round.contributions[msg.sender][uint256(_side)] += contribution; 749 | round.paidFees[uint256(_side)] += contribution; 750 | 751 | emit AppealContribution(_transactionID, _side, msg.sender, contribution); 752 | 753 | // Reimburse leftover ETH if any. 754 | // Deliberate use of send in order to not block the contract in case of reverting fallback. 755 | if (remainingETH > 0) payable(msg.sender).send(remainingETH); 756 | 757 | if (round.paidFees[uint256(_side)] >= totalCost) { 758 | if (round.sideFunded == Party.None) { 759 | round.sideFunded = _side; 760 | } else { 761 | // Both sides are fully funded. Create an appeal. 762 | arbitrator.appeal{ value: appealCost }(_transaction.disputeID, arbitratorExtraData); 763 | round.feeRewards = (round.paidFees[uint256(Party.Sender)] + 764 | round.paidFees[uint256(Party.Receiver)]).subCap(appealCost); 765 | roundsByTransactionID[_transactionID].push(); 766 | round.sideFunded = Party.None; 767 | } 768 | emit HasPaidAppealFee(_transactionID, _side); 769 | } 770 | } 771 | 772 | /** @dev Returns the contribution value and remainder from available ETH and required amount. 773 | * @param _available The amount of ETH available for the contribution. 774 | * @param _requiredAmount The amount of ETH required for the contribution. 775 | * @return taken The amount of ETH taken. 776 | * @return remainder The amount of ETH left from the contribution. 777 | */ 778 | function calculateContribution(uint256 _available, uint256 _requiredAmount) 779 | internal 780 | pure 781 | returns (uint256 taken, uint256 remainder) 782 | { 783 | // Take whatever is available, return 0 as leftover ETH. 784 | if (_requiredAmount > _available) return (_available, 0); 785 | 786 | remainder = _available - _requiredAmount; 787 | return (_requiredAmount, remainder); 788 | } 789 | 790 | /** @dev Updates contributions of appeal rounds which are going to be withdrawn. 791 | * Caller functions MUST: 792 | * (1) check that the transaction is valid and Resolved 793 | * (2) send the rewards to the _beneficiary. 794 | * @param _beneficiary The address that made contributions. 795 | * @param _transactionID The ID of the associated transaction. 796 | * @param _round The round from which to withdraw. 797 | * @param _finalRuling The final ruling of this transaction. 798 | * @return reward The amount of wei available to withdraw from _round. 799 | */ 800 | function _withdrawFeesAndRewards( 801 | address _beneficiary, 802 | uint256 _transactionID, 803 | uint256 _round, 804 | uint256 _finalRuling 805 | ) internal returns (uint256 reward) { 806 | Round storage round = roundsByTransactionID[_transactionID][_round]; 807 | uint256[3] storage contributionTo = round.contributions[_beneficiary]; 808 | uint256 lastRound = roundsByTransactionID[_transactionID].length - 1; 809 | 810 | if (_round == lastRound) { 811 | // Allow to reimburse if funding was unsuccessful. 812 | reward = 813 | contributionTo[uint256(Party.Sender)] + 814 | contributionTo[uint256(Party.Receiver)]; 815 | } else if (_finalRuling == uint256(Party.None)) { 816 | // Reimburse unspent fees proportionally if there is no winner and loser. 817 | uint256 totalFeesPaid = round.paidFees[uint256(Party.Sender)] + 818 | round.paidFees[uint256(Party.Receiver)]; 819 | uint256 totalBeneficiaryContributions = contributionTo[uint256(Party.Sender)] + 820 | contributionTo[uint256(Party.Receiver)]; 821 | reward = totalFeesPaid > 0 822 | ? (totalBeneficiaryContributions * round.feeRewards) / totalFeesPaid 823 | : 0; 824 | } else { 825 | // Reward the winner. 826 | reward = round.paidFees[_finalRuling] > 0 827 | ? (contributionTo[_finalRuling] * round.feeRewards) / round.paidFees[_finalRuling] 828 | : 0; 829 | } 830 | contributionTo[uint256(Party.Sender)] = 0; 831 | contributionTo[uint256(Party.Receiver)] = 0; 832 | } 833 | 834 | /** @dev Withdraws contributions of appeal rounds. Reimburses contributions 835 | * if the appeal was not fully funded. 836 | * If the appeal was fully funded, sends the fee stake rewards and reimbursements 837 | * proportional to the contributions made to the winner of a dispute. 838 | * @param _beneficiary The address that made contributions. 839 | * @param _transactionID The ID of the associated transaction. 840 | * @param _transaction The transaction state. 841 | * @param _round The round from which to withdraw. 842 | */ 843 | function withdrawFeesAndRewards( 844 | address payable _beneficiary, 845 | uint256 _transactionID, 846 | Transaction calldata _transaction, 847 | uint256 _round 848 | ) external onlyValidTransactionCD(_transactionID, _transaction) { 849 | require(_transaction.status == Status.Resolved, "The transaction must be resolved."); 850 | TransactionDispute storage transactionDispute = disputeIDtoTransactionDispute[ 851 | _transaction.disputeID 852 | ]; 853 | require(transactionDispute.transactionID == _transactionID, "Undisputed transaction"); 854 | 855 | uint256 reward = _withdrawFeesAndRewards( 856 | _beneficiary, 857 | _transactionID, 858 | _round, 859 | uint256(transactionDispute.ruling) 860 | ); 861 | _beneficiary.send(reward); // It is the user responsibility to accept ETH. 862 | } 863 | 864 | /** @dev Withdraws contributions of multiple appeal rounds at once. 865 | * This function is O(n) where n is the number of rounds. 866 | * This could exceed the gas limit, therefore this function should be used 867 | * only as a utility and not be relied upon by other contracts. 868 | * @param _beneficiary The address that made contributions. 869 | * @param _transactionID The ID of the associated transaction. 870 | * @param _transaction The transaction state. 871 | * @param _cursor The round from where to start withdrawing. 872 | * @param _count The number of rounds to iterate. If set to 0 or a value 873 | * larger than the number of rounds, iterates until the last round. 874 | */ 875 | function batchRoundWithdraw( 876 | address payable _beneficiary, 877 | uint256 _transactionID, 878 | Transaction calldata _transaction, 879 | uint256 _cursor, 880 | uint256 _count 881 | ) external onlyValidTransactionCD(_transactionID, _transaction) { 882 | require(_transaction.status == Status.Resolved, "The transaction must be resolved."); 883 | TransactionDispute storage transactionDispute = disputeIDtoTransactionDispute[ 884 | _transaction.disputeID 885 | ]; 886 | require(transactionDispute.transactionID == _transactionID, "Undisputed transaction"); 887 | uint256 finalRuling = uint256(transactionDispute.ruling); 888 | 889 | uint256 reward; 890 | uint256 totalRounds = roundsByTransactionID[_transactionID].length; 891 | for (uint256 i = _cursor; i < totalRounds && (_count == 0 || i < _cursor + _count); i++) 892 | reward += _withdrawFeesAndRewards(_beneficiary, _transactionID, i, finalRuling); 893 | _beneficiary.send(reward); // It is the user responsibility to accept ETH. 894 | } 895 | 896 | /** @dev Give a ruling for a dispute. Must be called by the arbitrator to 897 | * enforce the final ruling. The purpose of this function is to ensure that 898 | * the address calling it has the right to rule on the contract. 899 | * @param _disputeID ID of the dispute in the Arbitrator contract. 900 | * @param _ruling Ruling given by the arbitrator. Note that 0 is reserved 901 | * for "Not able/wanting to make a decision". 902 | */ 903 | function rule(uint256 _disputeID, uint256 _ruling) external override { 904 | require(msg.sender == address(arbitrator), "The caller must be the arbitrator."); 905 | require(_ruling <= AMOUNT_OF_CHOICES, "Invalid ruling."); 906 | 907 | TransactionDispute storage transactionDispute = disputeIDtoTransactionDispute[_disputeID]; 908 | require(transactionDispute.transactionID != 0, "Dispute does not exist."); 909 | require(transactionDispute.hasRuling == false, " Dispute already resolved."); 910 | 911 | Round[] storage rounds = roundsByTransactionID[transactionDispute.transactionID]; 912 | Round storage round = rounds[rounds.length - 1]; 913 | 914 | // If only one side paid its fees we assume the ruling to be in its favor. 915 | if (round.sideFunded == Party.Sender) transactionDispute.ruling = Party.Sender; 916 | else if (round.sideFunded == Party.Receiver) transactionDispute.ruling = Party.Receiver; 917 | else transactionDispute.ruling = Party(_ruling); 918 | 919 | transactionDispute.hasRuling = true; 920 | emit Ruling(arbitrator, _disputeID, uint256(transactionDispute.ruling)); 921 | } 922 | 923 | /** @dev Execute a ruling of a dispute. It reimburses the fee to the winning party. 924 | * @param _transactionID The index of the transaction. 925 | * @param _transaction The transaction state. 926 | */ 927 | function executeRuling(uint256 _transactionID, Transaction memory _transaction) 928 | external 929 | onlyValidTransaction(_transactionID, _transaction) 930 | { 931 | require(_transaction.status == Status.DisputeCreated, "Invalid transaction status."); 932 | 933 | TransactionDispute storage transactionDispute = disputeIDtoTransactionDispute[ 934 | _transaction.disputeID 935 | ]; 936 | require(transactionDispute.hasRuling, "Arbitrator has not ruled yet."); 937 | 938 | uint256 amount = _transaction.amount; 939 | uint256 settlementSender = _transaction.settlementSender; 940 | uint256 settlementReceiver = _transaction.settlementReceiver; 941 | uint256 senderFee = _transaction.senderFee; 942 | uint256 receiverFee = _transaction.receiverFee; 943 | 944 | _transaction.amount = 0; 945 | _transaction.settlementSender = 0; 946 | _transaction.settlementReceiver = 0; 947 | _transaction.senderFee = 0; 948 | _transaction.receiverFee = 0; 949 | _transaction.status = Status.Resolved; 950 | transactionHashes[_transactionID - 1] = hashTransactionState(_transaction); 951 | 952 | // Give the arbitration fee back. 953 | // Note that we use `send` to prevent a party from blocking the execution. 954 | if (transactionDispute.ruling == Party.Sender) { 955 | _transaction.sender.send(senderFee); 956 | 957 | // If there was a settlement amount proposed 958 | // we use that to make the partial payment and refund the rest to sender 959 | if (settlementSender != 0) { 960 | require( 961 | _transaction.token.transfer(_transaction.sender, amount - settlementSender), 962 | "The `transfer` function must not fail." 963 | ); 964 | require( 965 | _transaction.token.transfer(_transaction.receiver, settlementSender), 966 | "The `transfer` function must not fail." 967 | ); 968 | } else { 969 | require( 970 | _transaction.token.transfer(_transaction.sender, amount), 971 | "The `transfer` function must not fail." 972 | ); 973 | } 974 | } else if (transactionDispute.ruling == Party.Receiver) { 975 | _transaction.receiver.send(receiverFee); 976 | 977 | // If there was a settlement amount proposed 978 | // we use that to make the partial payment and refund the rest to sender 979 | if (settlementReceiver != 0) { 980 | require( 981 | _transaction.token.transfer(_transaction.sender, amount - settlementReceiver), 982 | "The `transfer` function must not fail." 983 | ); 984 | require( 985 | _transaction.token.transfer(_transaction.receiver, settlementReceiver), 986 | "The `transfer` function must not fail." 987 | ); 988 | } else { 989 | require( 990 | _transaction.token.transfer(_transaction.receiver, amount), 991 | "The `transfer` function must not fail." 992 | ); 993 | } 994 | } else { 995 | // `senderFee` and `receiverFee` are equal to the arbitration cost. 996 | uint256 splitArbitrationFee = senderFee / 2; 997 | _transaction.receiver.send(splitArbitrationFee); 998 | _transaction.sender.send(splitArbitrationFee); 999 | // Tokens should not reenter or allow recipients to refuse the transfer. 1000 | // In the case of an uneven token amount, one basic token unit can be burnt. 1001 | uint256 splitAmount = amount / 2; 1002 | require( 1003 | _transaction.token.transfer(_transaction.receiver, splitAmount), 1004 | "The `transfer` function must not fail." 1005 | ); 1006 | require( 1007 | _transaction.token.transfer(_transaction.sender, splitAmount), 1008 | "The `transfer` function must not fail." 1009 | ); 1010 | } 1011 | 1012 | emit TransactionStateUpdated(_transactionID, _transaction); 1013 | emit TransactionResolved(_transactionID, Resolution.RulingEnforced); 1014 | } 1015 | 1016 | // **************************** // 1017 | // * Constant getters * // 1018 | // **************************** // 1019 | 1020 | /** @dev Returns the sum of withdrawable wei from appeal rounds. 1021 | * This function is O(n), where n is the number of rounds of the transaction. 1022 | * This could exceed the gas limit, therefore this function should only 1023 | * be used for interface display and not by other contracts. 1024 | * @param _transactionID The index of the transaction. 1025 | * @param _transaction The transaction state. 1026 | * @param _beneficiary The contributor for which to query. 1027 | * @return total The total amount of wei available to withdraw. 1028 | */ 1029 | function amountWithdrawable( 1030 | uint256 _transactionID, 1031 | Transaction calldata _transaction, 1032 | address _beneficiary 1033 | ) external view onlyValidTransactionCD(_transactionID, _transaction) returns (uint256 total) { 1034 | if (_transaction.status != Status.Resolved) return total; 1035 | 1036 | TransactionDispute storage transactionDispute = disputeIDtoTransactionDispute[ 1037 | _transaction.disputeID 1038 | ]; 1039 | 1040 | if (transactionDispute.transactionID != _transactionID) return total; 1041 | uint256 finalRuling = uint256(transactionDispute.ruling); 1042 | 1043 | Round[] storage rounds = roundsByTransactionID[_transactionID]; 1044 | uint256 totalRounds = rounds.length; 1045 | for (uint256 i = 0; i < totalRounds; i++) { 1046 | Round storage round = rounds[i]; 1047 | if (i == totalRounds - 1) { 1048 | total += 1049 | round.contributions[_beneficiary][uint256(Party.Sender)] + 1050 | round.contributions[_beneficiary][uint256(Party.Receiver)]; 1051 | } else if (finalRuling == uint256(Party.None)) { 1052 | uint256 totalFeesPaid = round.paidFees[uint256(Party.Sender)] + 1053 | round.paidFees[uint256(Party.Receiver)]; 1054 | uint256 totalBeneficiaryContributions = round.contributions[_beneficiary][ 1055 | uint256(Party.Sender) 1056 | ] + round.contributions[_beneficiary][uint256(Party.Receiver)]; 1057 | total += totalFeesPaid > 0 1058 | ? (totalBeneficiaryContributions * round.feeRewards) / totalFeesPaid 1059 | : 0; 1060 | } else { 1061 | total += round.paidFees[finalRuling] > 0 1062 | ? (round.contributions[_beneficiary][finalRuling] * round.feeRewards) / 1063 | round.paidFees[finalRuling] 1064 | : 0; 1065 | } 1066 | } 1067 | } 1068 | 1069 | /** @dev Getter to know the count of transactions. 1070 | * @return The count of transactions. 1071 | */ 1072 | function getCountTransactions() external view returns (uint256) { 1073 | return transactionHashes.length; 1074 | } 1075 | 1076 | /** @dev Gets the number of rounds of the specific transaction. 1077 | * @param _transactionID The ID of the transaction. 1078 | * @return The number of rounds. 1079 | */ 1080 | function getNumberOfRounds(uint256 _transactionID) external view returns (uint256) { 1081 | return roundsByTransactionID[_transactionID].length; 1082 | } 1083 | 1084 | /** @dev Gets the contributions made by a party for a given round of the appeal. 1085 | * @param _transactionID The ID of the transaction. 1086 | * @param _round The position of the round. 1087 | * @param _contributor The address of the contributor. 1088 | * @return contributions The contributions. 1089 | */ 1090 | function getContributions( 1091 | uint256 _transactionID, 1092 | uint256 _round, 1093 | address _contributor 1094 | ) external view returns (uint256[3] memory contributions) { 1095 | Round storage round = roundsByTransactionID[_transactionID][_round]; 1096 | contributions = round.contributions[_contributor]; 1097 | } 1098 | 1099 | /** @dev Gets the information on a round of a transaction. 1100 | * @param _transactionID The ID of the transaction. 1101 | * @param _round The round to query. 1102 | * @return paidFees 1103 | * sideFunded 1104 | * feeRewards 1105 | * appealed 1106 | */ 1107 | function getRoundInfo(uint256 _transactionID, uint256 _round) 1108 | external 1109 | view 1110 | returns ( 1111 | uint256[3] memory paidFees, 1112 | Party sideFunded, 1113 | uint256 feeRewards, 1114 | bool appealed 1115 | ) 1116 | { 1117 | Round storage round = roundsByTransactionID[_transactionID][_round]; 1118 | return ( 1119 | round.paidFees, 1120 | round.sideFunded, 1121 | round.feeRewards, 1122 | _round != roundsByTransactionID[_transactionID].length - 1 1123 | ); 1124 | } 1125 | 1126 | /** 1127 | * @dev Gets the hashed version of the transaction state. 1128 | * If the caller function is using a Transaction object stored in calldata, 1129 | * this function is unnecessarily expensive, use hashTransactionStateCD instead. 1130 | * @param _transaction The transaction state. 1131 | * @return The hash of the transaction state. 1132 | */ 1133 | function hashTransactionState(Transaction memory _transaction) public pure returns (bytes32) { 1134 | return 1135 | keccak256( 1136 | abi.encodePacked( 1137 | _transaction.sender, 1138 | _transaction.receiver, 1139 | _transaction.amount, 1140 | _transaction.settlementSender, 1141 | _transaction.settlementReceiver, 1142 | _transaction.token, 1143 | _transaction.deadline, 1144 | _transaction.disputeID, 1145 | _transaction.senderFee, 1146 | _transaction.receiverFee, 1147 | _transaction.lastInteraction, 1148 | _transaction.status 1149 | ) 1150 | ); 1151 | } 1152 | 1153 | /** 1154 | * @dev Gets the hashed version of the transaction state. 1155 | * This function is cheap and can only be used when the caller function is 1156 | * using a Transaction object stored in calldata. 1157 | * @param _transaction The transaction state. 1158 | * @return The hash of the transaction state. 1159 | */ 1160 | function hashTransactionStateCD(Transaction calldata _transaction) 1161 | public 1162 | pure 1163 | returns (bytes32) 1164 | { 1165 | return 1166 | keccak256( 1167 | abi.encodePacked( 1168 | _transaction.sender, 1169 | _transaction.receiver, 1170 | _transaction.amount, 1171 | _transaction.settlementSender, 1172 | _transaction.settlementReceiver, 1173 | _transaction.token, 1174 | _transaction.deadline, 1175 | _transaction.disputeID, 1176 | _transaction.senderFee, 1177 | _transaction.receiverFee, 1178 | _transaction.lastInteraction, 1179 | _transaction.status 1180 | ) 1181 | ); 1182 | } 1183 | } 1184 | -------------------------------------------------------------------------------- /contracts/MultipleArbitrableTransactionWithAppeals.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | /** 4 | * @authors: [@unknownunknown1, @fnanni-0, @shalzz] 5 | * @reviewers: [@ferittuncer*, @epiqueras*, @nix1g*, @unknownunknown1, @alcercu*, @fnanni-0*] 6 | * @auditors: [] 7 | * @bounties: [] 8 | */ 9 | 10 | pragma solidity 0.8.9; 11 | 12 | import "@kleros/erc-792/contracts/IArbitrable.sol"; 13 | import "@kleros/erc-792/contracts/IArbitrator.sol"; 14 | import "@kleros/erc-792/contracts/erc-1497/IEvidence.sol"; 15 | import "./libraries/CappedMath.sol"; 16 | 17 | /** 18 | * @title MultipleArbitrableTransactionWithAppeals 19 | * @dev Escrow contract with appeal support. 20 | * Note that send() function is used deliberately instead of transfer() 21 | * to avoid blocking the flow with reverting fallback. 22 | */ 23 | contract MultipleArbitrableTransactionWithAppeals is IArbitrable, IEvidence { 24 | using CappedMath for uint256; 25 | // **************************** // 26 | // * Contract variables * // 27 | // **************************** // 28 | 29 | uint256 public constant AMOUNT_OF_CHOICES = 2; 30 | uint256 public constant MULTIPLIER_DIVISOR = 10000; // Divisor parameter for multipliers. 31 | 32 | enum Party { 33 | None, 34 | Sender, 35 | Receiver 36 | } 37 | 38 | enum Status { 39 | NoDispute, 40 | WaitingSettlementSender, 41 | WaitingSettlementReceiver, 42 | WaitingSender, 43 | WaitingReceiver, 44 | DisputeCreated, 45 | Resolved 46 | } 47 | 48 | enum Resolution { 49 | TransactionExecuted, 50 | TimeoutBySender, 51 | TimeoutByReceiver, 52 | RulingEnforced, 53 | SettlementReached 54 | } 55 | 56 | struct Transaction { 57 | address payable sender; 58 | address payable receiver; 59 | uint256 amount; 60 | uint256 settlementSender; // Settlement amount proposed by the sender 61 | uint256 settlementReceiver; // Settlement amount proposed by the receiver 62 | uint256 deadline; // Timestamp at which the transaction can be automatically executed if not disputed. 63 | uint256 disputeID; // If dispute exists, the ID of the dispute. 64 | uint256 senderFee; // Total fees paid by the sender. 65 | uint256 receiverFee; // Total fees paid by the receiver. 66 | uint256 lastInteraction; // Last interaction for the dispute procedure. 67 | Status status; 68 | } 69 | 70 | struct Round { 71 | uint256[3] paidFees; // Tracks the fees paid by each side in this round. 72 | // If the round is appealed, i.e. this is not the last round, Party.None means that both sides have paid. 73 | Party sideFunded; 74 | // Sum of reimbursable fees and stake rewards available to the parties 75 | // that made contributions to the side that ultimately wins a dispute. 76 | uint256 feeRewards; 77 | // Maps contributors to their contributions for each side. 78 | mapping(address => uint256[3]) contributions; 79 | } 80 | 81 | /** 82 | * @dev Tracks the state of eventual disputes. 83 | */ 84 | struct TransactionDispute { 85 | uint256 transactionID; // The transaction ID. 86 | bool hasRuling; // Required to differentiate between having no ruling and a RefusedToRule ruling. 87 | Party ruling; // The ruling given by the arbitrator. 88 | } 89 | 90 | IArbitrator public immutable arbitrator; // Address of the arbitrator contract. TRUSTED. 91 | bytes public arbitratorExtraData; // Extra data to set up the arbitration. 92 | // Time in seconds a party can take to pay arbitration fees before being 93 | // considered unresponsive and lose the dispute. 94 | uint256 public immutable feeTimeout; 95 | 96 | // Time in seconds a party can take to accept or propose a settlement 97 | // before being considered unresponsive and the case can be arbitrated. 98 | uint256 public immutable settlementTimeout; 99 | 100 | // Multiplier for calculating the appeal fee that must be paid by the 101 | // submitter in the case where there is no winner or loser 102 | // (e.g. when the arbitrator ruled "refuse to arbitrate"). 103 | uint256 public immutable sharedStakeMultiplier; 104 | // Multiplier for calculating the appeal fee of the party that won the previous round. 105 | uint256 public immutable winnerStakeMultiplier; 106 | // Multiplier for calculating the appeal fee of the party that lost the previous round. 107 | uint256 public immutable loserStakeMultiplier; 108 | 109 | /// @dev Stores the hashes of all transactions. 110 | bytes32[] public transactionHashes; 111 | 112 | /// @dev Maps a transactionID to its respective appeal rounds. 113 | mapping(uint256 => Round[]) public roundsByTransactionID; 114 | 115 | /// @dev Maps a disputeID to its respective transaction dispute. 116 | mapping(uint256 => TransactionDispute) public disputeIDtoTransactionDispute; 117 | 118 | // **************************** // 119 | // * Events * // 120 | // **************************** // 121 | 122 | /** 123 | * @dev To be emitted whenever a transaction state is updated. 124 | * @param _transactionID The ID of the changed transaction. 125 | * @param _transaction The full transaction data after update. 126 | */ 127 | event TransactionStateUpdated(uint256 indexed _transactionID, Transaction _transaction); 128 | 129 | /** @dev To be emitted when a party pays or reimburses the other. 130 | * @param _transactionID The index of the transaction. 131 | * @param _amount The amount paid. 132 | * @param _party The party that paid. 133 | */ 134 | event Payment(uint256 indexed _transactionID, uint256 _amount, address _party); 135 | 136 | /** @dev Indicate that a party has to pay a fee or would otherwise be considered as losing. 137 | * @param _transactionID The index of the transaction. 138 | * @param _party The party who has to pay. 139 | */ 140 | event HasToPayFee(uint256 indexed _transactionID, Party _party); 141 | 142 | /** @dev Emitted when a transaction is created. 143 | * @param _transactionID The index of the transaction. 144 | * @param _sender The address of the sender. 145 | * @param _receiver The address of the receiver. 146 | * @param _amount The initial amount in the transaction. 147 | */ 148 | event TransactionCreated( 149 | uint256 indexed _transactionID, 150 | address indexed _sender, 151 | address indexed _receiver, 152 | uint256 _amount 153 | ); 154 | 155 | /** @dev To be emitted when a transaction is resolved, either by its 156 | * execution, a timeout or because a ruling was enforced. 157 | * @param _transactionID The ID of the respective transaction. 158 | * @param _resolution Short description of what caused the transaction to be solved. 159 | */ 160 | event TransactionResolved(uint256 indexed _transactionID, Resolution indexed _resolution); 161 | 162 | /** @dev To be emitted when the appeal fees of one of the parties are fully funded. 163 | * @param _transactionID The ID of the respective transaction. 164 | * @param _party The party that is fully funded. 165 | */ 166 | event HasPaidAppealFee(uint256 indexed _transactionID, Party _party); 167 | 168 | /** 169 | * @dev To be emitted when someone contributes to the appeal process. 170 | * @param _transactionID The ID of the respective transaction. 171 | * @param _party The party which received the contribution. 172 | * @param _contributor The address of the contributor. 173 | * @param _amount The amount contributed. 174 | */ 175 | event AppealContribution( 176 | uint256 indexed _transactionID, 177 | Party _party, 178 | address _contributor, 179 | uint256 _amount 180 | ); 181 | 182 | // **************************** // 183 | // * Arbitrable functions * // 184 | // * Modifying the state * // 185 | // **************************** // 186 | 187 | /** @dev Constructor. 188 | * @param _arbitrator The arbitrator of the contract. 189 | * @param _arbitratorExtraData Extra data for the arbitrator. 190 | * @param _feeTimeout Arbitration fee timeout for the parties. 191 | * @param _settlementTimeout Settlement timeout for the parties. 192 | * @param _sharedStakeMultiplier Multiplier of the appeal cost that the 193 | * submitter must pay for a round when there is no winner/loser in the 194 | * previous round. In basis points. 195 | * @param _winnerStakeMultiplier Multiplier of the appeal cost that the 196 | * winner has to pay for a round. In basis points. 197 | * @param _loserStakeMultiplier Multiplier of the appeal cost that the 198 | * loser has to pay for a round. In basis points. 199 | */ 200 | constructor( 201 | IArbitrator _arbitrator, 202 | bytes memory _arbitratorExtraData, 203 | uint256 _feeTimeout, 204 | uint256 _settlementTimeout, 205 | uint256 _sharedStakeMultiplier, 206 | uint256 _winnerStakeMultiplier, 207 | uint256 _loserStakeMultiplier 208 | ) { 209 | arbitrator = _arbitrator; 210 | arbitratorExtraData = _arbitratorExtraData; 211 | feeTimeout = _feeTimeout; 212 | settlementTimeout = _settlementTimeout; 213 | sharedStakeMultiplier = _sharedStakeMultiplier; 214 | winnerStakeMultiplier = _winnerStakeMultiplier; 215 | loserStakeMultiplier = _loserStakeMultiplier; 216 | } 217 | 218 | modifier onlyValidTransaction(uint256 _transactionID, Transaction memory _transaction) { 219 | require( 220 | transactionHashes[_transactionID - 1] == hashTransactionState(_transaction), 221 | "Transaction doesn't match stored hash." 222 | ); 223 | _; 224 | } 225 | 226 | /// @dev Using calldata as data location makes gas consumption more efficient 227 | /// when caller function also uses calldata. 228 | modifier onlyValidTransactionCD(uint256 _transactionID, Transaction calldata _transaction) { 229 | require( 230 | transactionHashes[_transactionID - 1] == hashTransactionStateCD(_transaction), 231 | "Transaction doesn't match stored hash." 232 | ); 233 | _; 234 | } 235 | 236 | /** @dev Create a transaction. 237 | * @param _timeoutPayment Time after which a party can automatically execute the arbitrable transaction. 238 | * @param _receiver The recipient of the transaction. 239 | * @param _metaEvidence Link to the meta-evidence. 240 | * @return transactionID The index of the transaction. 241 | */ 242 | function createTransaction( 243 | uint256 _timeoutPayment, 244 | address payable _receiver, 245 | string calldata _metaEvidence 246 | ) external payable returns (uint256 transactionID) { 247 | Transaction memory transaction; 248 | transaction.sender = payable(msg.sender); 249 | transaction.receiver = _receiver; 250 | transaction.amount = msg.value; 251 | transaction.deadline = block.timestamp.addCap(_timeoutPayment); 252 | 253 | transactionHashes.push(hashTransactionState(transaction)); 254 | // transactionID starts at 1. This way, TransactionDispute can check 255 | // if a dispute exists by testing transactionID != 0. 256 | transactionID = transactionHashes.length; 257 | 258 | emit TransactionCreated(transactionID, msg.sender, _receiver, msg.value); 259 | emit TransactionStateUpdated(transactionID, transaction); 260 | emit MetaEvidence(transactionID, _metaEvidence); 261 | } 262 | 263 | /** @dev Pay receiver. To be called if the good or service is provided. 264 | * @param _transactionID The index of the transaction. 265 | * @param _transaction The transaction state. 266 | * @param _amount Amount to pay in wei. 267 | */ 268 | function pay( 269 | uint256 _transactionID, 270 | Transaction memory _transaction, 271 | uint256 _amount 272 | ) external onlyValidTransaction(_transactionID, _transaction) { 273 | require(_transaction.sender == msg.sender, "The caller must be the sender."); 274 | require(_transaction.status == Status.NoDispute, "The transaction must not be disputed."); 275 | require(_amount <= _transaction.amount, "Maximum amount available for payment exceeded."); 276 | 277 | _transaction.receiver.send(_amount); // It is the user responsibility to accept ETH. 278 | _transaction.amount -= _amount; 279 | 280 | transactionHashes[_transactionID - 1] = hashTransactionState(_transaction); // solhint-disable-line 281 | emit Payment(_transactionID, _amount, msg.sender); 282 | emit TransactionStateUpdated(_transactionID, _transaction); 283 | } 284 | 285 | /** @dev Reimburse sender. To be called if the good or service can't be fully provided. 286 | * @param _transactionID The index of the transaction. 287 | * @param _transaction The transaction state. 288 | * @param _amountReimbursed Amount to reimburse in wei. 289 | */ 290 | function reimburse( 291 | uint256 _transactionID, 292 | Transaction memory _transaction, 293 | uint256 _amountReimbursed 294 | ) external onlyValidTransaction(_transactionID, _transaction) { 295 | require(_transaction.receiver == msg.sender, "The caller must be the receiver."); 296 | require(_transaction.status == Status.NoDispute, "The transaction must not be disputed."); 297 | require( 298 | _amountReimbursed <= _transaction.amount, 299 | "Maximum reimbursement available exceeded." 300 | ); 301 | 302 | _transaction.sender.send(_amountReimbursed); // It is the user responsibility to accept ETH. 303 | _transaction.amount -= _amountReimbursed; 304 | 305 | transactionHashes[_transactionID - 1] = hashTransactionState(_transaction); // solhint-disable-line 306 | emit Payment(_transactionID, _amountReimbursed, msg.sender); 307 | emit TransactionStateUpdated(_transactionID, _transaction); 308 | } 309 | 310 | /** @dev Transfer the transaction's amount to the receiver if the timeout has passed. 311 | * @param _transactionID The index of the transaction. 312 | * @param _transaction The transaction state. 313 | */ 314 | function executeTransaction(uint256 _transactionID, Transaction memory _transaction) 315 | external 316 | onlyValidTransaction(_transactionID, _transaction) 317 | { 318 | require(block.timestamp >= _transaction.deadline, "Deadline not passed."); 319 | require(_transaction.status == Status.NoDispute, "The transaction must not be disputed."); 320 | 321 | _transaction.receiver.send(_transaction.amount); // It is the user responsibility to accept ETH. 322 | _transaction.amount = 0; 323 | 324 | _transaction.status = Status.Resolved; 325 | 326 | transactionHashes[_transactionID - 1] = hashTransactionState(_transaction); // solhint-disable-line 327 | emit TransactionStateUpdated(_transactionID, _transaction); 328 | emit TransactionResolved(_transactionID, Resolution.TransactionExecuted); 329 | } 330 | 331 | /** @notice Propose a settlement as a compromise from the initial terms to the other party. 332 | * @dev A party can only propose a settlement again after the other party has 333 | * done so as well to prevent front running/griefing issues. 334 | * @param _transactionID The index of the transaction. 335 | * @param _transaction The transaction state. 336 | * @param _amount The settlement amount. 337 | */ 338 | function proposeSettlement( 339 | uint256 _transactionID, 340 | Transaction memory _transaction, 341 | uint256 _amount 342 | ) external onlyValidTransaction(_transactionID, _transaction) { 343 | require( 344 | block.timestamp < _transaction.deadline || _transaction.status != Status.NoDispute, 345 | "Transaction expired" 346 | ); 347 | require( 348 | _transaction.status < Status.WaitingSender, 349 | "Transaction already escalated for arbitration" 350 | ); 351 | 352 | require( 353 | _amount <= _transaction.amount, 354 | "Settlement amount cannot be more that the initial amount" 355 | ); 356 | 357 | if (_transaction.status == Status.WaitingSettlementSender) { 358 | require(msg.sender == _transaction.sender, "The caller must be the sender."); 359 | _transaction.settlementSender = _amount; 360 | _transaction.status = Status.WaitingSettlementReceiver; 361 | } else if (_transaction.status == Status.WaitingSettlementReceiver) { 362 | require(msg.sender == _transaction.receiver, "The caller must be the receiver."); 363 | _transaction.settlementReceiver = _amount; 364 | _transaction.status = Status.WaitingSettlementSender; 365 | } else { 366 | if (msg.sender == _transaction.sender) { 367 | _transaction.settlementSender = _amount; 368 | _transaction.status = Status.WaitingSettlementReceiver; 369 | } else if (msg.sender == _transaction.receiver) { 370 | _transaction.settlementReceiver = _amount; 371 | _transaction.status = Status.WaitingSettlementSender; 372 | } else revert("Only the sender or receiver addresses are authorized"); 373 | } 374 | 375 | _transaction.lastInteraction = block.timestamp; 376 | transactionHashes[_transactionID - 1] = hashTransactionState(_transaction); 377 | emit TransactionStateUpdated(_transactionID, _transaction); 378 | } 379 | 380 | /** @notice Accept a settlement proposed by the other party. 381 | * @param _transactionID The index of the transaction. 382 | * @param _transaction The transaction state. 383 | */ 384 | function acceptSettlement(uint256 _transactionID, Transaction memory _transaction) 385 | external 386 | onlyValidTransaction(_transactionID, _transaction) 387 | { 388 | uint256 settlementAmount; 389 | if (_transaction.status == Status.WaitingSettlementSender) { 390 | require(msg.sender == _transaction.sender, "The caller must be the sender."); 391 | settlementAmount = _transaction.settlementReceiver; 392 | } else if (_transaction.status == Status.WaitingSettlementReceiver) { 393 | require(msg.sender == _transaction.receiver, "The caller must be the receiver."); 394 | settlementAmount = _transaction.settlementSender; 395 | } else revert("No settlement proposed to accept or tx already disputed/resolved."); 396 | 397 | uint256 remainingAmount = _transaction.amount - settlementAmount; 398 | 399 | _transaction.amount = 0; 400 | _transaction.settlementSender = 0; 401 | _transaction.settlementReceiver = 0; 402 | 403 | _transaction.status = Status.Resolved; 404 | 405 | // It is the users responsibility to accept ETH. 406 | _transaction.sender.send(remainingAmount); 407 | _transaction.receiver.send(settlementAmount); 408 | 409 | transactionHashes[_transactionID - 1] = hashTransactionState(_transaction); // solhint-disable-line 410 | emit TransactionStateUpdated(_transactionID, _transaction); 411 | emit TransactionResolved(_transactionID, Resolution.SettlementReached); 412 | } 413 | 414 | /** @dev Pay the arbitration fee to raise a dispute. To be called by the sender. UNTRUSTED. 415 | * Note that the arbitrator can have createDispute throw, which will make 416 | * this function throw and therefore lead to a party being timed-out. 417 | * This is not a vulnerability as the arbitrator can rule in favor of one party anyway. 418 | * @param _transactionID The index of the transaction. 419 | * @param _transaction The transaction state. 420 | */ 421 | function payArbitrationFeeBySender(uint256 _transactionID, Transaction memory _transaction) 422 | external 423 | payable 424 | onlyValidTransaction(_transactionID, _transaction) 425 | { 426 | require( 427 | _transaction.status == Status.WaitingSettlementSender || 428 | _transaction.status == Status.WaitingSettlementReceiver || 429 | _transaction.status == Status.WaitingSender, 430 | "Settlement not attempted first or the transaction already executed/disputed." 431 | ); 432 | 433 | // Allow the other party enough time to respond to a settlement before 434 | // allowing the proposer to raise a dispute. 435 | if (_transaction.status == Status.WaitingSettlementReceiver) { 436 | require( 437 | block.timestamp - _transaction.lastInteraction >= settlementTimeout, 438 | "Settlement period has not timed out yet." 439 | ); 440 | } 441 | 442 | require(msg.sender == _transaction.sender, "The caller must be the sender."); 443 | 444 | uint256 arbitrationCost = arbitrator.arbitrationCost(arbitratorExtraData); 445 | _transaction.senderFee += msg.value; 446 | // Require that the total paid to be at least the arbitration cost. 447 | require( 448 | _transaction.senderFee >= arbitrationCost, 449 | "The sender fee must cover arbitration costs." 450 | ); 451 | 452 | _transaction.lastInteraction = block.timestamp; 453 | 454 | // The receiver still has to pay. This can also happen if he has paid, 455 | // but arbitrationCost has increased. 456 | if (_transaction.receiverFee < arbitrationCost) { 457 | _transaction.status = Status.WaitingReceiver; 458 | emit HasToPayFee(_transactionID, Party.Receiver); 459 | } else { 460 | // The receiver has also paid the fee. We create the dispute. 461 | raiseDispute(_transactionID, _transaction, arbitrationCost); 462 | } 463 | 464 | transactionHashes[_transactionID - 1] = hashTransactionState(_transaction); 465 | emit TransactionStateUpdated(_transactionID, _transaction); 466 | } 467 | 468 | /** @dev Pay the arbitration fee to raise a dispute. To be called by the receiver. UNTRUSTED. 469 | * Note that this function mirrors payArbitrationFeeBySender. 470 | * @param _transactionID The index of the transaction. 471 | * @param _transaction The transaction state. 472 | */ 473 | function payArbitrationFeeByReceiver(uint256 _transactionID, Transaction memory _transaction) 474 | external 475 | payable 476 | onlyValidTransaction(_transactionID, _transaction) 477 | { 478 | require( 479 | _transaction.status == Status.WaitingSettlementSender || 480 | _transaction.status == Status.WaitingSettlementReceiver || 481 | _transaction.status == Status.WaitingReceiver, 482 | "Settlement not attempted first or the transaction already executed/disputed." 483 | ); 484 | 485 | // Allow the other party enough time to respond to a settlement before 486 | // allowing the proposer to raise a dispute. 487 | if (_transaction.status == Status.WaitingSettlementSender) { 488 | require( 489 | block.timestamp - _transaction.lastInteraction >= settlementTimeout, 490 | "Settlement period has not timed out yet." 491 | ); 492 | } 493 | 494 | require(msg.sender == _transaction.receiver, "The caller must be the receiver."); 495 | 496 | uint256 arbitrationCost = arbitrator.arbitrationCost(arbitratorExtraData); 497 | _transaction.receiverFee += msg.value; 498 | // Require that the total paid to be at least the arbitration cost. 499 | require( 500 | _transaction.receiverFee >= arbitrationCost, 501 | "The receiver fee must cover arbitration costs." 502 | ); 503 | 504 | _transaction.lastInteraction = block.timestamp; 505 | // The sender still has to pay. This can also happen if he has paid, 506 | // but arbitrationCost has increased. 507 | if (_transaction.senderFee < arbitrationCost) { 508 | _transaction.status = Status.WaitingSender; 509 | emit HasToPayFee(_transactionID, Party.Sender); 510 | } else { 511 | // The sender has also paid the fee. We create the dispute. 512 | raiseDispute(_transactionID, _transaction, arbitrationCost); 513 | } 514 | 515 | transactionHashes[_transactionID - 1] = hashTransactionState(_transaction); 516 | emit TransactionStateUpdated(_transactionID, _transaction); 517 | } 518 | 519 | /** @dev Reimburse sender if receiver fails to pay the fee. 520 | * @param _transactionID The index of the transaction. 521 | * @param _transaction The transaction state. 522 | */ 523 | function timeOutBySender(uint256 _transactionID, Transaction memory _transaction) 524 | external 525 | onlyValidTransaction(_transactionID, _transaction) 526 | { 527 | require( 528 | _transaction.status == Status.WaitingReceiver, 529 | "The transaction is not waiting on the receiver." 530 | ); 531 | require( 532 | block.timestamp - _transaction.lastInteraction >= feeTimeout, 533 | "Timeout time has not passed yet." 534 | ); 535 | 536 | if (_transaction.receiverFee != 0) { 537 | _transaction.receiver.send(_transaction.receiverFee); // It is the user responsibility to accept ETH. 538 | _transaction.receiverFee = 0; 539 | } 540 | 541 | // It is the user responsibility to accept ETH. 542 | _transaction.sender.send(_transaction.senderFee + _transaction.amount); 543 | _transaction.amount = 0; 544 | _transaction.settlementSender = 0; 545 | _transaction.settlementReceiver = 0; 546 | _transaction.senderFee = 0; 547 | _transaction.status = Status.Resolved; 548 | 549 | transactionHashes[_transactionID - 1] = hashTransactionState(_transaction); // solhint-disable-line 550 | emit TransactionStateUpdated(_transactionID, _transaction); 551 | emit TransactionResolved(_transactionID, Resolution.TimeoutBySender); 552 | } 553 | 554 | /** @dev Pay receiver if sender fails to pay the fee. 555 | * @param _transactionID The index of the transaction. 556 | * @param _transaction The transaction state. 557 | */ 558 | function timeOutByReceiver(uint256 _transactionID, Transaction memory _transaction) 559 | external 560 | onlyValidTransaction(_transactionID, _transaction) 561 | { 562 | require( 563 | _transaction.status == Status.WaitingSender, 564 | "The transaction is not waiting on the sender." 565 | ); 566 | require( 567 | block.timestamp - _transaction.lastInteraction >= feeTimeout, 568 | "Timeout time has not passed yet." 569 | ); 570 | 571 | if (_transaction.senderFee != 0) { 572 | _transaction.sender.send(_transaction.senderFee); // It is the user responsibility to accept ETH. 573 | _transaction.senderFee = 0; 574 | } 575 | 576 | // It is the user responsibility to accept ETH. 577 | _transaction.receiver.send(_transaction.receiverFee + _transaction.amount); 578 | _transaction.amount = 0; 579 | _transaction.settlementSender = 0; 580 | _transaction.settlementReceiver = 0; 581 | _transaction.receiverFee = 0; 582 | _transaction.status = Status.Resolved; 583 | 584 | transactionHashes[_transactionID - 1] = hashTransactionState(_transaction); // solhint-disable-line 585 | emit TransactionStateUpdated(_transactionID, _transaction); 586 | emit TransactionResolved(_transactionID, Resolution.TimeoutByReceiver); 587 | } 588 | 589 | /** @dev Create a dispute. UNTRUSTED. 590 | * This function is internal and thus the transaction state validity is 591 | * not checked. Caller functions MUST do the check before calling this function. 592 | * _transaction MUST be a reference (not a copy) because its state is 593 | * modified. Caller functions MUST emit the TransactionStateUpdated event and update the hash. 594 | * @param _transactionID The index of the transaction. 595 | * @param _transaction The transaction state. 596 | * @param _arbitrationCost Amount to pay the arbitrator. 597 | */ 598 | function raiseDispute( 599 | uint256 _transactionID, 600 | Transaction memory _transaction, 601 | uint256 _arbitrationCost 602 | ) internal { 603 | _transaction.status = Status.DisputeCreated; 604 | _transaction.disputeID = arbitrator.createDispute{ value: _arbitrationCost }( 605 | AMOUNT_OF_CHOICES, 606 | arbitratorExtraData 607 | ); 608 | roundsByTransactionID[_transactionID].push(); 609 | TransactionDispute storage transactionDispute = disputeIDtoTransactionDispute[ 610 | _transaction.disputeID 611 | ]; 612 | transactionDispute.transactionID = _transactionID; 613 | emit Dispute(arbitrator, _transaction.disputeID, _transactionID, _transactionID); 614 | 615 | // Refund sender if it overpaid. 616 | if (_transaction.senderFee > _arbitrationCost) { 617 | uint256 extraFeeSender = _transaction.senderFee - _arbitrationCost; 618 | _transaction.senderFee = _arbitrationCost; 619 | _transaction.sender.send(extraFeeSender); // It is the user responsibility to accept ETH. 620 | } 621 | 622 | // Refund receiver if it overpaid. 623 | if (_transaction.receiverFee > _arbitrationCost) { 624 | uint256 extraFeeReceiver = _transaction.receiverFee - _arbitrationCost; 625 | _transaction.receiverFee = _arbitrationCost; 626 | _transaction.receiver.send(extraFeeReceiver); // It is the user responsibility to accept ETH. 627 | } 628 | } 629 | 630 | /** @dev Submit a reference to evidence. EVENT. 631 | * @param _transactionID The index of the transaction. 632 | * @param _transaction The transaction state. 633 | * @param _evidence A link to an evidence using its URI. 634 | */ 635 | function submitEvidence( 636 | uint256 _transactionID, 637 | Transaction calldata _transaction, 638 | string calldata _evidence 639 | ) external onlyValidTransactionCD(_transactionID, _transaction) { 640 | require( 641 | _transaction.status < Status.Resolved, 642 | "Must not send evidence if the dispute is resolved." 643 | ); 644 | 645 | emit Evidence(arbitrator, _transactionID, msg.sender, _evidence); 646 | } 647 | 648 | /** @dev Takes up to the total amount required to fund a side of an appeal. 649 | * Reimburses the rest. Creates an appeal if both sides are fully funded. 650 | * @param _transactionID The ID of the disputed transaction. 651 | * @param _transaction The transaction state. 652 | * @param _side The party that pays the appeal fee. 653 | */ 654 | function fundAppeal( 655 | uint256 _transactionID, 656 | Transaction calldata _transaction, 657 | Party _side 658 | ) external payable onlyValidTransactionCD(_transactionID, _transaction) { 659 | require(_side != Party.None, "Wrong party."); 660 | require(_transaction.status == Status.DisputeCreated, "No dispute to appeal"); 661 | 662 | (uint256 appealPeriodStart, uint256 appealPeriodEnd) = arbitrator.appealPeriod( 663 | _transaction.disputeID 664 | ); 665 | require( 666 | block.timestamp >= appealPeriodStart && block.timestamp < appealPeriodEnd, 667 | "Funding must be made within the appeal period." 668 | ); 669 | 670 | uint256 multiplier; 671 | uint256 winner = arbitrator.currentRuling(_transaction.disputeID); 672 | if (winner == uint256(_side)) { 673 | multiplier = winnerStakeMultiplier; 674 | } else if (winner == 0) { 675 | multiplier = sharedStakeMultiplier; 676 | } else { 677 | require( 678 | block.timestamp < (appealPeriodEnd + appealPeriodStart) / 2, 679 | "The loser must pay during the first half of the appeal period." 680 | ); 681 | multiplier = loserStakeMultiplier; 682 | } 683 | 684 | Round storage round = roundsByTransactionID[_transactionID][ 685 | roundsByTransactionID[_transactionID].length - 1 686 | ]; 687 | require(_side != round.sideFunded, "Appeal fee has already been paid."); 688 | 689 | uint256 appealCost = arbitrator.appealCost(_transaction.disputeID, arbitratorExtraData); 690 | uint256 totalCost = appealCost.addCap((appealCost.mulCap(multiplier)) / MULTIPLIER_DIVISOR); 691 | 692 | // Take up to the amount necessary to fund the current round at the current costs. 693 | (uint256 contribution, uint256 remainingETH) = calculateContribution( 694 | msg.value, 695 | totalCost.subCap(round.paidFees[uint256(_side)]) 696 | ); 697 | round.contributions[msg.sender][uint256(_side)] += contribution; 698 | round.paidFees[uint256(_side)] += contribution; 699 | 700 | emit AppealContribution(_transactionID, _side, msg.sender, contribution); 701 | 702 | // Reimburse leftover ETH if any. 703 | if (remainingETH > 0) payable(msg.sender).send(remainingETH); // It is the user responsibility to accept ETH. 704 | 705 | if (round.paidFees[uint256(_side)] >= totalCost) { 706 | if (round.sideFunded == Party.None) { 707 | round.sideFunded = _side; 708 | } else { 709 | // Both sides are fully funded. Create an appeal. 710 | arbitrator.appeal{ value: appealCost }(_transaction.disputeID, arbitratorExtraData); 711 | round.feeRewards = (round.paidFees[uint256(Party.Sender)] + 712 | round.paidFees[uint256(Party.Receiver)]).subCap(appealCost); 713 | roundsByTransactionID[_transactionID].push(); 714 | round.sideFunded = Party.None; 715 | } 716 | emit HasPaidAppealFee(_transactionID, _side); 717 | } 718 | } 719 | 720 | /** @dev Returns the contribution value and remainder from available ETH and required amount. 721 | * @param _available The amount of ETH available for the contribution. 722 | * @param _requiredAmount The amount of ETH required for the contribution. 723 | * @return taken The amount of ETH taken. 724 | * @return remainder The amount of ETH left from the contribution. 725 | */ 726 | function calculateContribution(uint256 _available, uint256 _requiredAmount) 727 | internal 728 | pure 729 | returns (uint256 taken, uint256 remainder) 730 | { 731 | // Take whatever is available, return 0 as leftover ETH. 732 | if (_requiredAmount > _available) return (_available, 0); 733 | 734 | remainder = _available - _requiredAmount; 735 | return (_requiredAmount, remainder); 736 | } 737 | 738 | /** @dev Updates the state of contributions of appeal rounds which are going to be withdrawn. 739 | * Caller functions MUST: 740 | * (1) check that the transaction is valid and Resolved 741 | * (2) send the rewards to the _beneficiary. 742 | * @param _beneficiary The address that made contributions. 743 | * @param _transactionID The ID of the associated transaction. 744 | * @param _round The round from which to withdraw. 745 | * @param _finalRuling The final ruling of this transaction. 746 | * @return reward The amount of wei available to withdraw from _round. 747 | */ 748 | function _withdrawFeesAndRewards( 749 | address _beneficiary, 750 | uint256 _transactionID, 751 | uint256 _round, 752 | uint256 _finalRuling 753 | ) internal returns (uint256 reward) { 754 | Round storage round = roundsByTransactionID[_transactionID][_round]; 755 | uint256[3] storage contributionTo = round.contributions[_beneficiary]; 756 | uint256 lastRound = roundsByTransactionID[_transactionID].length - 1; 757 | 758 | if (_round == lastRound) { 759 | // Allow to reimburse if funding was unsuccessful. 760 | reward = 761 | contributionTo[uint256(Party.Sender)] + 762 | contributionTo[uint256(Party.Receiver)]; 763 | } else if (_finalRuling == uint256(Party.None)) { 764 | // Reimburse unspent fees proportionally if there is no winner and loser. 765 | uint256 totalFeesPaid = round.paidFees[uint256(Party.Sender)] + 766 | round.paidFees[uint256(Party.Receiver)]; 767 | uint256 totalBeneficiaryContributions = contributionTo[uint256(Party.Sender)] + 768 | contributionTo[uint256(Party.Receiver)]; 769 | reward = totalFeesPaid > 0 770 | ? (totalBeneficiaryContributions * round.feeRewards) / totalFeesPaid 771 | : 0; 772 | } else { 773 | // Reward the winner. 774 | reward = round.paidFees[_finalRuling] > 0 775 | ? (contributionTo[_finalRuling] * round.feeRewards) / round.paidFees[_finalRuling] 776 | : 0; 777 | } 778 | contributionTo[uint256(Party.Sender)] = 0; 779 | contributionTo[uint256(Party.Receiver)] = 0; 780 | } 781 | 782 | /** @dev Withdraws contributions of appeal rounds. Reimburses contributions 783 | * if the appeal was not fully funded. 784 | * If the appeal was fully funded, sends the fee stake rewards and 785 | * reimbursements proportional to the contributions made to the winner of a dispute. 786 | * @param _beneficiary The address that made contributions. 787 | * @param _transactionID The ID of the associated transaction. 788 | * @param _transaction The transaction state. 789 | * @param _round The round from which to withdraw. 790 | */ 791 | function withdrawFeesAndRewards( 792 | address payable _beneficiary, 793 | uint256 _transactionID, 794 | Transaction calldata _transaction, 795 | uint256 _round 796 | ) external onlyValidTransactionCD(_transactionID, _transaction) { 797 | require(_transaction.status == Status.Resolved, "The transaction must be resolved."); 798 | TransactionDispute storage transactionDispute = disputeIDtoTransactionDispute[ 799 | _transaction.disputeID 800 | ]; 801 | require(transactionDispute.transactionID == _transactionID, "Undisputed transaction"); 802 | 803 | uint256 reward = _withdrawFeesAndRewards( 804 | _beneficiary, 805 | _transactionID, 806 | _round, 807 | uint256(transactionDispute.ruling) 808 | ); 809 | _beneficiary.send(reward); // It is the user responsibility to accept ETH. 810 | } 811 | 812 | /** @dev Withdraws contributions of multiple appeal rounds at once. 813 | * This function is O(n) where n is the number of rounds. 814 | * This could exceed the gas limit, therefore this function should be used 815 | * only as a utility and not be relied upon by other contracts. 816 | * @param _beneficiary The address that made contributions. 817 | * @param _transactionID The ID of the associated transaction. 818 | * @param _transaction The transaction state. 819 | * @param _cursor The round from where to start withdrawing. 820 | * @param _count The number of rounds to iterate. If set to 0 or a value 821 | * larger than the number of rounds, iterates until the last round. 822 | */ 823 | function batchRoundWithdraw( 824 | address payable _beneficiary, 825 | uint256 _transactionID, 826 | Transaction calldata _transaction, 827 | uint256 _cursor, 828 | uint256 _count 829 | ) external onlyValidTransactionCD(_transactionID, _transaction) { 830 | require(_transaction.status == Status.Resolved, "The transaction must be resolved."); 831 | TransactionDispute storage transactionDispute = disputeIDtoTransactionDispute[ 832 | _transaction.disputeID 833 | ]; 834 | require(transactionDispute.transactionID == _transactionID, "Undisputed transaction"); 835 | uint256 finalRuling = uint256(transactionDispute.ruling); 836 | 837 | uint256 reward; 838 | uint256 totalRounds = roundsByTransactionID[_transactionID].length; 839 | for (uint256 i = _cursor; i < totalRounds && (_count == 0 || i < _cursor + _count); i++) 840 | reward += _withdrawFeesAndRewards(_beneficiary, _transactionID, i, finalRuling); 841 | _beneficiary.send(reward); // It is the user responsibility to accept ETH. 842 | } 843 | 844 | /** @dev Give a ruling for a dispute. Must be called by the arbitrator to 845 | * enforce the final ruling. The purpose of this function is to ensure that 846 | * the address calling it has the right to rule on the contract. 847 | * @param _disputeID ID of the dispute in the Arbitrator contract. 848 | * @param _ruling Ruling given by the arbitrator. Note that 0 is reserved 849 | * for "Not able/wanting to make a decision". 850 | */ 851 | function rule(uint256 _disputeID, uint256 _ruling) external override { 852 | require(msg.sender == address(arbitrator), "The caller must be the arbitrator."); 853 | require(_ruling <= AMOUNT_OF_CHOICES, "Invalid ruling."); 854 | 855 | TransactionDispute storage transactionDispute = disputeIDtoTransactionDispute[_disputeID]; 856 | require(transactionDispute.transactionID != 0, "Dispute does not exist."); 857 | require(transactionDispute.hasRuling == false, " Dispute already resolved."); 858 | 859 | Round[] storage rounds = roundsByTransactionID[transactionDispute.transactionID]; 860 | Round storage round = rounds[rounds.length - 1]; 861 | 862 | // If only one side paid its fees we assume the ruling to be in its favor. 863 | if (round.sideFunded == Party.Sender) transactionDispute.ruling = Party.Sender; 864 | else if (round.sideFunded == Party.Receiver) transactionDispute.ruling = Party.Receiver; 865 | else transactionDispute.ruling = Party(_ruling); 866 | 867 | transactionDispute.hasRuling = true; 868 | emit Ruling(arbitrator, _disputeID, uint256(transactionDispute.ruling)); 869 | } 870 | 871 | /** @dev Execute a ruling of a dispute. It reimburses the fee to the winning party. 872 | * @param _transactionID The index of the transaction. 873 | * @param _transaction The transaction state. 874 | */ 875 | function executeRuling(uint256 _transactionID, Transaction memory _transaction) 876 | external 877 | onlyValidTransaction(_transactionID, _transaction) 878 | { 879 | require(_transaction.status == Status.DisputeCreated, "Invalid transaction status."); 880 | 881 | TransactionDispute storage transactionDispute = disputeIDtoTransactionDispute[ 882 | _transaction.disputeID 883 | ]; 884 | require(transactionDispute.hasRuling, "Arbitrator has not ruled yet."); 885 | 886 | // Give the arbitration fee back. 887 | // Note that we use send to prevent a party from blocking the execution. 888 | if (transactionDispute.ruling == Party.Sender) { 889 | // If there was a settlement amount proposed 890 | // we use that to make the partial payment and refund the rest 891 | if (_transaction.settlementSender != 0) { 892 | _transaction.sender.send( 893 | _transaction.senderFee + _transaction.amount - _transaction.settlementSender 894 | ); 895 | _transaction.receiver.send(_transaction.settlementSender); 896 | } else { 897 | _transaction.sender.send(_transaction.senderFee + _transaction.amount); 898 | } 899 | } else if (transactionDispute.ruling == Party.Receiver) { 900 | // If there was a settlement amount proposed 901 | // we use that to make the partial payment and refund the rest to sender 902 | if (_transaction.settlementReceiver != 0) { 903 | _transaction.sender.send(_transaction.amount - _transaction.settlementReceiver); 904 | _transaction.receiver.send( 905 | _transaction.receiverFee + _transaction.settlementReceiver 906 | ); 907 | } else { 908 | _transaction.receiver.send(_transaction.receiverFee + _transaction.amount); 909 | } 910 | } else { 911 | uint256 splitAmount = (_transaction.senderFee + _transaction.amount) / 2; 912 | _transaction.sender.send(splitAmount); 913 | _transaction.receiver.send(splitAmount); 914 | } 915 | 916 | _transaction.amount = 0; 917 | _transaction.settlementSender = 0; 918 | _transaction.settlementReceiver = 0; 919 | _transaction.senderFee = 0; 920 | _transaction.receiverFee = 0; 921 | _transaction.status = Status.Resolved; 922 | 923 | transactionHashes[_transactionID - 1] = hashTransactionState(_transaction); // solhint-disable-line 924 | emit TransactionStateUpdated(_transactionID, _transaction); 925 | emit TransactionResolved(_transactionID, Resolution.RulingEnforced); 926 | } 927 | 928 | // **************************** // 929 | // * Constant getters * // 930 | // **************************** // 931 | 932 | /** @dev Returns the sum of withdrawable wei from appeal rounds. 933 | * This function is O(n), where n is the number of rounds of the transaction. 934 | * This could exceed the gas limit, therefore this function should only 935 | * be used for interface display and not by other contracts. 936 | * @param _transactionID The index of the transaction. 937 | * @param _transaction The transaction state. 938 | * @param _beneficiary The contributor for which to query. 939 | * @return total The total amount of wei available to withdraw. 940 | */ 941 | function amountWithdrawable( 942 | uint256 _transactionID, 943 | Transaction calldata _transaction, 944 | address _beneficiary 945 | ) external view onlyValidTransactionCD(_transactionID, _transaction) returns (uint256 total) { 946 | if (_transaction.status != Status.Resolved) return total; 947 | 948 | TransactionDispute storage transactionDispute = disputeIDtoTransactionDispute[ 949 | _transaction.disputeID 950 | ]; 951 | 952 | if (transactionDispute.transactionID != _transactionID) return total; 953 | uint256 finalRuling = uint256(transactionDispute.ruling); 954 | 955 | Round[] storage rounds = roundsByTransactionID[_transactionID]; 956 | uint256 totalRounds = rounds.length; 957 | for (uint256 i = 0; i < totalRounds; i++) { 958 | Round storage round = rounds[i]; 959 | if (i == totalRounds - 1) { 960 | total += 961 | round.contributions[_beneficiary][uint256(Party.Sender)] + 962 | round.contributions[_beneficiary][uint256(Party.Receiver)]; 963 | } else if (finalRuling == uint256(Party.None)) { 964 | uint256 totalFeesPaid = round.paidFees[uint256(Party.Sender)] + 965 | round.paidFees[uint256(Party.Receiver)]; 966 | uint256 totalBeneficiaryContributions = round.contributions[_beneficiary][ 967 | uint256(Party.Sender) 968 | ] + round.contributions[_beneficiary][uint256(Party.Receiver)]; 969 | total += totalFeesPaid > 0 970 | ? (totalBeneficiaryContributions * round.feeRewards) / totalFeesPaid 971 | : 0; 972 | } else { 973 | total += round.paidFees[finalRuling] > 0 974 | ? (round.contributions[_beneficiary][finalRuling] * round.feeRewards) / 975 | round.paidFees[finalRuling] 976 | : 0; 977 | } 978 | } 979 | } 980 | 981 | /** @dev Getter to know the count of transactions. 982 | * @return The count of transactions. 983 | */ 984 | function getCountTransactions() external view returns (uint256) { 985 | return transactionHashes.length; 986 | } 987 | 988 | /** @dev Gets the number of rounds of the specific transaction. 989 | * @param _transactionID The ID of the transaction. 990 | * @return The number of rounds. 991 | */ 992 | function getNumberOfRounds(uint256 _transactionID) external view returns (uint256) { 993 | return roundsByTransactionID[_transactionID].length; 994 | } 995 | 996 | /** @dev Gets the contributions made by a party for a given round of the appeal. 997 | * @param _transactionID The ID of the transaction. 998 | * @param _round The position of the round. 999 | * @param _contributor The address of the contributor. 1000 | * @return contributions The contributions. 1001 | */ 1002 | function getContributions( 1003 | uint256 _transactionID, 1004 | uint256 _round, 1005 | address _contributor 1006 | ) external view returns (uint256[3] memory contributions) { 1007 | Round storage round = roundsByTransactionID[_transactionID][_round]; 1008 | contributions = round.contributions[_contributor]; 1009 | } 1010 | 1011 | /** @dev Gets the information on a round of a transaction. 1012 | * @param _transactionID The ID of the transaction. 1013 | * @param _round The round to query. 1014 | * @return paidFees 1015 | * sideFunded 1016 | * feeRewards 1017 | * appealed 1018 | */ 1019 | function getRoundInfo(uint256 _transactionID, uint256 _round) 1020 | external 1021 | view 1022 | returns ( 1023 | uint256[3] memory paidFees, 1024 | Party sideFunded, 1025 | uint256 feeRewards, 1026 | bool appealed 1027 | ) 1028 | { 1029 | Round storage round = roundsByTransactionID[_transactionID][_round]; 1030 | return ( 1031 | round.paidFees, 1032 | round.sideFunded, 1033 | round.feeRewards, 1034 | _round != roundsByTransactionID[_transactionID].length - 1 1035 | ); 1036 | } 1037 | 1038 | /** 1039 | * @dev Gets the hashed version of the transaction state. 1040 | * If the caller function is using a Transaction object stored in calldata, 1041 | * this function is unnecessarily expensive, use hashTransactionStateCD instead. 1042 | * @param _transaction The transaction state. 1043 | * @return The hash of the transaction state. 1044 | */ 1045 | function hashTransactionState(Transaction memory _transaction) public pure returns (bytes32) { 1046 | return 1047 | keccak256( 1048 | abi.encodePacked( 1049 | _transaction.sender, 1050 | _transaction.receiver, 1051 | _transaction.amount, 1052 | _transaction.settlementSender, 1053 | _transaction.settlementReceiver, 1054 | _transaction.deadline, 1055 | _transaction.disputeID, 1056 | _transaction.senderFee, 1057 | _transaction.receiverFee, 1058 | _transaction.lastInteraction, 1059 | _transaction.status 1060 | ) 1061 | ); 1062 | } 1063 | 1064 | /** 1065 | * @dev Gets the hashed version of the transaction state. 1066 | * This function is cheap and can only be used when the caller function is 1067 | * using a Transaction object stored in calldata. 1068 | * @param _transaction The transaction state. 1069 | * @return The hash of the transaction state. 1070 | */ 1071 | function hashTransactionStateCD(Transaction calldata _transaction) 1072 | public 1073 | pure 1074 | returns (bytes32) 1075 | { 1076 | return 1077 | keccak256( 1078 | abi.encodePacked( 1079 | _transaction.sender, 1080 | _transaction.receiver, 1081 | _transaction.amount, 1082 | _transaction.settlementSender, 1083 | _transaction.settlementReceiver, 1084 | _transaction.deadline, 1085 | _transaction.disputeID, 1086 | _transaction.senderFee, 1087 | _transaction.receiverFee, 1088 | _transaction.lastInteraction, 1089 | _transaction.status 1090 | ) 1091 | ); 1092 | } 1093 | } 1094 | -------------------------------------------------------------------------------- /contracts/libraries/CappedMath.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | /** 6 | * @title CappedMath 7 | * @dev Math operations with caps for under and overflow. 8 | */ 9 | library CappedMath { 10 | uint256 private constant UINT_MAX = type(uint256).max; 11 | 12 | /** 13 | * @dev Adds two unsigned integers, returns 2^256 - 1 on overflow. 14 | */ 15 | function addCap(uint256 _a, uint256 _b) internal pure returns (uint256) { 16 | unchecked { 17 | uint256 c = _a + _b; 18 | return c >= _a ? c : UINT_MAX; 19 | } 20 | } 21 | 22 | /** 23 | * @dev Subtracts two integers, returns 0 on underflow. 24 | */ 25 | function subCap(uint256 _a, uint256 _b) internal pure returns (uint256) { 26 | if (_b > _a) return 0; 27 | else return _a - _b; 28 | } 29 | 30 | /** 31 | * @dev Multiplies two unsigned integers, returns 2^256 - 1 on overflow. 32 | */ 33 | function mulCap(uint256 _a, uint256 _b) internal pure returns (uint256) { 34 | // Gas optimization: this is cheaper than requiring '_a' not being zero, but the 35 | // benefit is lost if '_b' is also tested. 36 | // See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522 37 | if (_a == 0) return 0; 38 | 39 | unchecked { 40 | uint256 c = _a * _b; 41 | return c / _a == _b ? c : UINT_MAX; 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /contracts/mocks/ERC20Mock.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | import { ERC20PresetMinterPauser } from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; 4 | 5 | // mock class using ERC20 6 | contract ERC20Mock is ERC20PresetMinterPauser { 7 | constructor(address initialAccount, uint256 initialBalance, string memory name, string memory symbol) ERC20PresetMinterPauser(name, symbol) { 8 | mint(initialAccount, initialBalance); 9 | } 10 | } -------------------------------------------------------------------------------- /contracts/mocks/TestArbitrator.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | /** @title IArbitrable 4 | * @author Enrique Piqueras - 5 | * Arbitrable interface. 6 | * When developing arbitrable contracts, we need to: 7 | * -Define the action taken when a ruling is received by the contract. We should do so in executeRuling. 8 | * -Allow dispute creation. For this a function must: 9 | * -Call arbitrator.createDispute.value(_fee)(_choices,_extraData); 10 | * -Create the event Dispute(_arbitrator,_disputeID,_rulingOptions); 11 | */ 12 | interface IArbitrable { 13 | /** @dev To be emmited when meta-evidence is submitted. 14 | * @param _metaEvidenceID Unique identifier of meta-evidence. 15 | * @param _evidence A link to the meta-evidence JSON. 16 | */ 17 | event MetaEvidence(uint indexed _metaEvidenceID, string _evidence); 18 | 19 | /** @dev To be emmited when a dispute is created to link the correct meta-evidence to the disputeID 20 | * @param _arbitrator The arbitrator of the contract. 21 | * @param _disputeID ID of the dispute in the Arbitrator contract. 22 | * @param _metaEvidenceID Unique identifier of meta-evidence. 23 | * @param _evidenceGroupID Unique identifier of the evidence group that is linked to this dispute. 24 | */ 25 | event Dispute(Arbitrator indexed _arbitrator, uint indexed _disputeID, uint _metaEvidenceID, uint _evidenceGroupID); 26 | 27 | /** @dev To be raised when evidence are submitted. Should point to the ressource (evidences are not to be stored on chain due to gas considerations). 28 | * @param _arbitrator The arbitrator of the contract. 29 | * @param _evidenceGroupID Unique identifier of the evidence group the evidence belongs to. 30 | * @param _party The address of the party submiting the evidence. Note that 0x0 refers to evidence not submitted by any party. 31 | * @param _evidence A URI to the evidence JSON file whose name should be its keccak256 hash followed by .json. 32 | */ 33 | event Evidence(Arbitrator indexed _arbitrator, uint indexed _evidenceGroupID, address indexed _party, string _evidence); 34 | 35 | /** @dev To be raised when a ruling is given. 36 | * @param _arbitrator The arbitrator giving the ruling. 37 | * @param _disputeID ID of the dispute in the Arbitrator contract. 38 | * @param _ruling The ruling which was given. 39 | */ 40 | event Ruling(Arbitrator indexed _arbitrator, uint indexed _disputeID, uint _ruling); 41 | 42 | /** @dev Give a ruling for a dispute. Must be called by the arbitrator. 43 | * The purpose of this function is to ensure that the address calling it has the right to rule on the contract. 44 | * @param _disputeID ID of the dispute in the Arbitrator contract. 45 | * @param _ruling Ruling given by the arbitrator. Note that 0 is reserved for "Not able/wanting to make a decision". 46 | */ 47 | function rule(uint _disputeID, uint _ruling) external; 48 | } 49 | 50 | /** @title Arbitrable 51 | * @author Clément Lesaege - 52 | * Arbitrable abstract contract. 53 | * When developing arbitrable contracts, we need to: 54 | * -Define the action taken when a ruling is received by the contract. We should do so in executeRuling. 55 | * -Allow dispute creation. For this a function must: 56 | * -Call arbitrator.createDispute.value(_fee)(_choices,_extraData); 57 | * -Create the event Dispute(_arbitrator,_disputeID,_rulingOptions); 58 | */ 59 | abstract contract Arbitrable is IArbitrable { 60 | Arbitrator public arbitrator; 61 | bytes public arbitratorExtraData; // Extra data to require particular dispute and appeal behaviour. 62 | 63 | modifier onlyArbitrator {require(msg.sender == address(arbitrator), "Can only be called by the arbitrator."); _;} 64 | 65 | /** @dev Constructor. Choose the arbitrator. 66 | * @param _arbitrator The arbitrator of the contract. 67 | * @param _arbitratorExtraData Extra data for the arbitrator. 68 | */ 69 | constructor(Arbitrator _arbitrator, bytes memory _arbitratorExtraData) { 70 | arbitrator = _arbitrator; 71 | arbitratorExtraData = _arbitratorExtraData; 72 | } 73 | 74 | /** @dev Give a ruling for a dispute. Must be called by the arbitrator. 75 | * The purpose of this function is to ensure that the address calling it has the right to rule on the contract. 76 | * @param _disputeID ID of the dispute in the Arbitrator contract. 77 | * @param _ruling Ruling given by the arbitrator. Note that 0 is reserved for "Not able/wanting to make a decision". 78 | */ 79 | function rule(uint _disputeID, uint _ruling) public onlyArbitrator { 80 | emit Ruling(Arbitrator(msg.sender),_disputeID,_ruling); 81 | 82 | executeRuling(_disputeID,_ruling); 83 | } 84 | 85 | 86 | /** @dev Execute a ruling of a dispute. 87 | * @param _disputeID ID of the dispute in the Arbitrator contract. 88 | * @param _ruling Ruling given by the arbitrator. Note that 0 is reserved for "Not able/wanting to make a decision". 89 | */ 90 | function executeRuling(uint _disputeID, uint _ruling) internal virtual; 91 | } 92 | 93 | /** @title Arbitrator 94 | * @author Clément Lesaege - 95 | * Arbitrator abstract contract. 96 | * When developing arbitrator contracts we need to: 97 | * -Define the functions for dispute creation (createDispute) and appeal (appeal). Don't forget to store the arbitrated contract and the disputeID (which should be unique, use nbDisputes). 98 | * -Define the functions for cost display (arbitrationCost and appealCost). 99 | * -Allow giving rulings. For this a function must call arbitrable.rule(disputeID, ruling). 100 | */ 101 | abstract contract Arbitrator { 102 | 103 | enum DisputeStatus {Waiting, Appealable, Solved} 104 | 105 | modifier requireArbitrationFee(bytes memory _extraData) { 106 | require(msg.value >= arbitrationCost(_extraData), "Not enough ETH to cover arbitration costs."); 107 | _; 108 | } 109 | modifier requireAppealFee(uint _disputeID, bytes memory _extraData) { 110 | require(msg.value >= appealCost(_disputeID, _extraData), "Not enough ETH to cover appeal costs."); 111 | _; 112 | } 113 | 114 | /** @dev To be raised when a dispute is created. 115 | * @param _disputeID ID of the dispute. 116 | * @param _arbitrable The contract which created the dispute. 117 | */ 118 | event DisputeCreation(uint indexed _disputeID, Arbitrable indexed _arbitrable); 119 | 120 | /** @dev To be raised when a dispute can be appealed. 121 | * @param _disputeID ID of the dispute. 122 | * @param _arbitrable The contract which created the dispute. 123 | */ 124 | event AppealPossible(uint indexed _disputeID, Arbitrable indexed _arbitrable); 125 | 126 | /** @dev To be raised when the current ruling is appealed. 127 | * @param _disputeID ID of the dispute. 128 | * @param _arbitrable The contract which created the dispute. 129 | */ 130 | event AppealDecision(uint indexed _disputeID, Arbitrable indexed _arbitrable); 131 | 132 | /** @dev Create a dispute. Must be called by the arbitrable contract. 133 | * Must be paid at least arbitrationCost(_extraData). 134 | * @param _choices Amount of choices the arbitrator can make in this dispute. 135 | * @param _extraData Can be used to give additional info on the dispute to be created. 136 | * @return disputeID ID of the dispute created. 137 | */ 138 | function createDispute(uint _choices, bytes memory _extraData) public requireArbitrationFee(_extraData) virtual payable returns(uint disputeID) {} 139 | 140 | /** @dev Compute the cost of arbitration. It is recommended not to increase it often, as it can be highly time and gas consuming for the arbitrated contracts to cope with fee augmentation. 141 | * @param _extraData Can be used to give additional info on the dispute to be created. 142 | * @return fee Amount to be paid. 143 | */ 144 | function arbitrationCost(bytes memory _extraData) public virtual view returns(uint fee); 145 | 146 | /** @dev Appeal a ruling. Note that it has to be called before the arbitrator contract calls rule. 147 | * @param _disputeID ID of the dispute to be appealed. 148 | * @param _extraData Can be used to give extra info on the appeal. 149 | */ 150 | function appeal(uint _disputeID, bytes memory _extraData) public virtual requireAppealFee(_disputeID,_extraData) payable { 151 | emit AppealDecision(_disputeID, Arbitrable(msg.sender)); 152 | } 153 | 154 | /** @dev Compute the cost of appeal. It is recommended not to increase it often, as it can be higly time and gas consuming for the arbitrated contracts to cope with fee augmentation. 155 | * @param _disputeID ID of the dispute to be appealed. 156 | * @param _extraData Can be used to give additional info on the dispute to be created. 157 | * @return fee Amount to be paid. 158 | */ 159 | function appealCost(uint _disputeID, bytes memory _extraData) public virtual view returns(uint fee); 160 | 161 | /** @dev Compute the start and end of the dispute's current or next appeal period, if possible. 162 | * @param _disputeID ID of the dispute. 163 | * @return start The start of the period. 164 | * @return end The end of the period. 165 | */ 166 | function appealPeriod(uint _disputeID) public virtual view returns(uint start, uint end) {} 167 | 168 | /** @dev Return the status of a dispute. 169 | * @param _disputeID ID of the dispute to rule. 170 | * @return status The status of the dispute. 171 | */ 172 | function disputeStatus(uint _disputeID) public virtual view returns(DisputeStatus status); 173 | 174 | /** @dev Return the current ruling of a dispute. This is useful for parties to know if they should appeal. 175 | * @param _disputeID ID of the dispute. 176 | * @return ruling The ruling which has been given or the one which will be given if there is no appeal. 177 | */ 178 | function currentRuling(uint _disputeID) public virtual view returns(uint ruling); 179 | } 180 | 181 | /** @title Centralized Arbitrator 182 | * @dev This is a centralized arbitrator deciding alone on the result of disputes. No appeals are possible. 183 | */ 184 | contract CentralizedArbitrator is Arbitrator { 185 | 186 | address public owner = msg.sender; 187 | uint arbitrationPrice; // Not public because arbitrationCost already acts as an accessor. 188 | uint constant NOT_PAYABLE_VALUE = (2**256-2)/2; // High value to be sure that the appeal is too expensive. 189 | 190 | struct DisputeStruct { 191 | Arbitrable arbitrated; 192 | uint choices; 193 | uint fee; 194 | uint ruling; 195 | DisputeStatus status; 196 | } 197 | 198 | modifier onlyOwner {require(msg.sender==owner, "Can only be called by the owner."); _;} 199 | 200 | DisputeStruct[] public disputes; 201 | 202 | /** @dev Constructor. Set the initial arbitration price. 203 | * @param _arbitrationPrice Amount to be paid for arbitration. 204 | */ 205 | constructor(uint _arbitrationPrice) public { 206 | arbitrationPrice = _arbitrationPrice; 207 | } 208 | 209 | /** @dev Set the arbitration price. Only callable by the owner. 210 | * @param _arbitrationPrice Amount to be paid for arbitration. 211 | */ 212 | function setArbitrationPrice(uint _arbitrationPrice) public onlyOwner { 213 | arbitrationPrice = _arbitrationPrice; 214 | } 215 | 216 | /** @dev Cost of arbitration. Accessor to arbitrationPrice. 217 | * @param _extraData Not used by this contract. 218 | * @return fee Amount to be paid. 219 | */ 220 | function arbitrationCost(bytes memory _extraData) public view override returns(uint fee) { 221 | return arbitrationPrice; 222 | } 223 | 224 | /** @dev Cost of appeal. Since it is not possible, it's a high value which can never be paid. 225 | * @param _disputeID ID of the dispute to be appealed. Not used by this contract. 226 | * @param _extraData Not used by this contract. 227 | * @return fee Amount to be paid. 228 | */ 229 | function appealCost(uint _disputeID, bytes memory _extraData) public view virtual override returns(uint fee) { 230 | return NOT_PAYABLE_VALUE; 231 | } 232 | 233 | /** @dev Create a dispute. Must be called by the arbitrable contract. 234 | * Must be paid at least arbitrationCost(). 235 | * @param _choices Amount of choices the arbitrator can make in this dispute. When ruling ruling<=choices. 236 | * @param _extraData Can be used to give additional info on the dispute to be created. 237 | * @return disputeID ID of the dispute created. 238 | */ 239 | function createDispute(uint _choices, bytes memory _extraData) public payable override returns(uint disputeID) { 240 | super.createDispute(_choices, _extraData); 241 | disputeID= disputes.length; 242 | DisputeStruct storage dispute = disputes.push(); 243 | dispute.arbitrated = Arbitrable(msg.sender); 244 | dispute.choices = _choices; 245 | dispute.fee = msg.value; 246 | dispute.ruling = 0; 247 | dispute.status = DisputeStatus.Waiting; 248 | emit DisputeCreation(disputeID, Arbitrable(msg.sender)); 249 | } 250 | 251 | /** @dev Give a ruling. UNTRUSTED. 252 | * @param _disputeID ID of the dispute to rule. 253 | * @param _ruling Ruling given by the arbitrator. Note that 0 means "Not able/wanting to make a decision". 254 | */ 255 | function _giveRuling(uint _disputeID, uint _ruling) internal { 256 | DisputeStruct storage dispute = disputes[_disputeID]; 257 | require(_ruling <= dispute.choices, "Invalid ruling."); 258 | require(dispute.status != DisputeStatus.Solved, "The dispute must not be solved already."); 259 | 260 | dispute.ruling = _ruling; 261 | dispute.status = DisputeStatus.Solved; 262 | 263 | payable(msg.sender).send(dispute.fee); // Avoid blocking. 264 | dispute.arbitrated.rule(_disputeID,_ruling); 265 | } 266 | 267 | /** @dev Give a ruling. UNTRUSTED. 268 | * @param _disputeID ID of the dispute to rule. 269 | * @param _ruling Ruling given by the arbitrator. Note that 0 means "Not able/wanting to make a decision". 270 | */ 271 | function giveRuling(uint _disputeID, uint _ruling) public virtual onlyOwner { 272 | return _giveRuling(_disputeID, _ruling); 273 | } 274 | 275 | /** @dev Return the status of a dispute. 276 | * @param _disputeID ID of the dispute to rule. 277 | * @return status The status of the dispute. 278 | */ 279 | function disputeStatus(uint _disputeID) public view virtual override returns(DisputeStatus status) { 280 | return disputes[_disputeID].status; 281 | } 282 | 283 | /** @dev Return the ruling of a dispute. 284 | * @param _disputeID ID of the dispute to rule. 285 | * @return ruling The ruling which would or has been given. 286 | */ 287 | function currentRuling(uint _disputeID) public view virtual override returns(uint ruling) { 288 | return disputes[_disputeID].ruling; 289 | } 290 | } 291 | 292 | /** 293 | * @title AppealableArbitrator 294 | * @dev A centralized arbitrator that can be appealed. 295 | */ 296 | contract AppealableArbitrator is CentralizedArbitrator, Arbitrable { 297 | /* Structs */ 298 | 299 | struct AppealDispute { 300 | uint rulingTime; 301 | Arbitrator arbitrator; 302 | uint appealDisputeID; 303 | } 304 | 305 | /* Storage */ 306 | 307 | uint public timeOut; 308 | mapping(uint => AppealDispute) public appealDisputes; 309 | mapping(uint => uint) public appealDisputeIDsToDisputeIDs; 310 | 311 | /* Constructor */ 312 | 313 | /** @dev Constructs the `AppealableArbitrator` contract. 314 | * @param _arbitrationPrice The amount to be paid for arbitration. 315 | * @param _arbitrator The back up arbitrator. 316 | * @param _arbitratorExtraData Not used by this contract. 317 | * @param _timeOut The time out for the appeal period. 318 | */ 319 | constructor( 320 | uint _arbitrationPrice, 321 | Arbitrator _arbitrator, 322 | bytes memory _arbitratorExtraData, 323 | uint _timeOut 324 | ) public CentralizedArbitrator(_arbitrationPrice) Arbitrable(_arbitrator, _arbitratorExtraData) { 325 | timeOut = _timeOut; 326 | } 327 | 328 | /* External */ 329 | 330 | /** @dev Changes the back up arbitrator. 331 | * @param _arbitrator The new back up arbitrator. 332 | */ 333 | function changeArbitrator(Arbitrator _arbitrator) external onlyOwner { 334 | arbitrator = _arbitrator; 335 | } 336 | 337 | /** @dev Changes the time out. 338 | * @param _timeOut The new time out. 339 | */ 340 | function changeTimeOut(uint _timeOut) external onlyOwner { 341 | timeOut = _timeOut; 342 | } 343 | 344 | /* External Views */ 345 | 346 | /** @dev Gets the specified dispute's latest appeal ID. 347 | * @param _disputeID The ID of the dispute. 348 | */ 349 | function getAppealDisputeID(uint _disputeID) external view returns(uint disputeID) { 350 | if (appealDisputes[_disputeID].arbitrator != Arbitrator(address(0))) 351 | disputeID = AppealableArbitrator(address(appealDisputes[_disputeID].arbitrator)).getAppealDisputeID(appealDisputes[_disputeID].appealDisputeID); 352 | else disputeID = _disputeID; 353 | } 354 | 355 | /* Public */ 356 | 357 | /** @dev Appeals a ruling. 358 | * @param _disputeID The ID of the dispute. 359 | * @param _extraData Additional info about the appeal. 360 | */ 361 | function appeal(uint _disputeID, bytes memory _extraData) public payable override requireAppealFee(_disputeID, _extraData) { 362 | super.appeal(_disputeID, _extraData); 363 | if (appealDisputes[_disputeID].arbitrator != Arbitrator(address(0))) 364 | appealDisputes[_disputeID].arbitrator.appeal{value: msg.value}(appealDisputes[_disputeID].appealDisputeID, _extraData); 365 | else { 366 | appealDisputes[_disputeID].arbitrator = arbitrator; 367 | appealDisputes[_disputeID].appealDisputeID = arbitrator.createDispute{value: msg.value}(disputes[_disputeID].choices, _extraData); 368 | appealDisputeIDsToDisputeIDs[appealDisputes[_disputeID].appealDisputeID] = _disputeID; 369 | } 370 | } 371 | 372 | /** @dev Gives a ruling. 373 | * @param _disputeID The ID of the dispute. 374 | * @param _ruling The ruling. 375 | */ 376 | function giveRuling(uint _disputeID, uint _ruling) public override { 377 | require(disputes[_disputeID].status != DisputeStatus.Solved, "The specified dispute is already resolved."); 378 | if (appealDisputes[_disputeID].arbitrator != Arbitrator(address(0))) { 379 | require(Arbitrator(msg.sender) == appealDisputes[_disputeID].arbitrator, "Appealed disputes must be ruled by their back up arbitrator."); 380 | super._giveRuling(_disputeID, _ruling); 381 | } else { 382 | require(msg.sender == owner, "Not appealed disputes must be ruled by the owner."); 383 | if (disputes[_disputeID].status == DisputeStatus.Appealable) { 384 | if (block.timestamp - appealDisputes[_disputeID].rulingTime > timeOut) 385 | super._giveRuling(_disputeID, disputes[_disputeID].ruling); 386 | else revert("Time out time has not passed yet."); 387 | } else { 388 | disputes[_disputeID].ruling = _ruling; 389 | disputes[_disputeID].status = DisputeStatus.Appealable; 390 | appealDisputes[_disputeID].rulingTime = block.timestamp; 391 | emit AppealPossible(_disputeID, disputes[_disputeID].arbitrated); 392 | } 393 | } 394 | } 395 | 396 | /* Public Views */ 397 | 398 | /** @dev Gets the cost of appeal for the specified dispute. 399 | * @param _disputeID The ID of the dispute. 400 | * @param _extraData Additional info about the appeal. 401 | * @return cost The cost of the appeal. 402 | */ 403 | function appealCost(uint _disputeID, bytes memory _extraData) public view override returns(uint cost) { 404 | if (appealDisputes[_disputeID].arbitrator != Arbitrator(address(0))) 405 | cost = appealDisputes[_disputeID].arbitrator.appealCost(appealDisputes[_disputeID].appealDisputeID, _extraData); 406 | else if (disputes[_disputeID].status == DisputeStatus.Appealable) cost = arbitrator.arbitrationCost(_extraData); 407 | else cost = NOT_PAYABLE_VALUE; 408 | } 409 | 410 | /** @dev Gets the status of the specified dispute. 411 | * @param _disputeID The ID of the dispute. 412 | * @return status The status. 413 | */ 414 | function disputeStatus(uint _disputeID) public view override returns(DisputeStatus status) { 415 | if (appealDisputes[_disputeID].arbitrator != Arbitrator(address(0))) 416 | status = appealDisputes[_disputeID].arbitrator.disputeStatus(appealDisputes[_disputeID].appealDisputeID); 417 | else status = disputes[_disputeID].status; 418 | } 419 | 420 | /** @dev Return the ruling of a dispute. 421 | * @param _disputeID ID of the dispute to rule. 422 | * @return ruling The ruling which would or has been given. 423 | */ 424 | function currentRuling(uint _disputeID) public view override returns(uint ruling) { 425 | if (appealDisputes[_disputeID].arbitrator != Arbitrator(address(0))) // Appealed. 426 | ruling = appealDisputes[_disputeID].arbitrator.currentRuling(appealDisputes[_disputeID].appealDisputeID); // Retrieve ruling from the arbitrator whom the dispute is appealed to. 427 | else ruling = disputes[_disputeID].ruling; // Not appealed, basic case. 428 | } 429 | 430 | /* Internal */ 431 | 432 | /** @dev Executes the ruling of the specified dispute. 433 | * @param _disputeID The ID of the dispute. 434 | * @param _ruling The ruling. 435 | */ 436 | function executeRuling(uint _disputeID, uint _ruling) internal override { 437 | require( 438 | appealDisputes[appealDisputeIDsToDisputeIDs[_disputeID]].arbitrator != Arbitrator(address(0)), 439 | "The dispute must have been appealed." 440 | ); 441 | giveRuling(appealDisputeIDsToDisputeIDs[_disputeID], _ruling); 442 | } 443 | } 444 | 445 | /** 446 | * @title EnhancedAppealableArbitrator 447 | * @author Enrique Piqueras - 448 | * @dev Implementation of `AppealableArbitrator` that supports `appealPeriod`. 449 | */ 450 | contract EnhancedAppealableArbitrator is AppealableArbitrator { 451 | /* Constructor */ 452 | 453 | /** @dev Constructs the `EnhancedAppealableArbitrator` contract. 454 | * @param _arbitrationPrice The amount to be paid for arbitration. 455 | * @param _arbitrator The back up arbitrator. 456 | * @param _arbitratorExtraData Not used by this contract. 457 | * @param _timeOut The time out for the appeal period. 458 | */ 459 | constructor( 460 | uint _arbitrationPrice, 461 | Arbitrator _arbitrator, 462 | bytes memory _arbitratorExtraData, 463 | uint _timeOut 464 | ) public AppealableArbitrator(_arbitrationPrice, _arbitrator, _arbitratorExtraData, _timeOut) {} 465 | 466 | /* Public Views */ 467 | 468 | /** @dev Compute the start and end of the dispute's current or next appeal period, if possible. 469 | * @param _disputeID ID of the dispute. 470 | * @return start The start of the period. 471 | * @return end The end of the period. 472 | */ 473 | function appealPeriod(uint _disputeID) public view override returns(uint start, uint end) { 474 | if (appealDisputes[_disputeID].arbitrator != Arbitrator(address(0))) 475 | (start, end) = appealDisputes[_disputeID].arbitrator.appealPeriod(appealDisputes[_disputeID].appealDisputeID); 476 | else { 477 | start = appealDisputes[_disputeID].rulingTime; 478 | require(start != 0, "The specified dispute is not appealable."); 479 | end = start + timeOut; 480 | } 481 | } 482 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "escrow-contracts", 3 | "version": "0.1.0", 4 | "description": "Smart contracts interacting with Kleros.", 5 | "main": "index.js", 6 | "author": "Kleros", 7 | "license": "MIT", 8 | "keywords": [ 9 | "kleros", 10 | "escrow", 11 | "arbitration", 12 | "arbitrable", 13 | "arbitrator" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/kleros/escrow-contracts.git" 18 | }, 19 | "private": false, 20 | "files": [ 21 | "contracts", 22 | "build" 23 | ], 24 | "scripts": { 25 | "build": "buidler compile", 26 | "clean": "buidler clean", 27 | "lint": "prettier --write '**/*.*(sol|js|json|md)'", 28 | "lint:check": "prettier --check '**/*.*(sol|js|json|md)'", 29 | "lint:sol": "solhint 'contracts/**/*.sol'", 30 | "b:test": "buidler test", 31 | "pretest": "run-s -s build", 32 | "test": "mocha --timeout 10000 -r @nomiclabs/buidler/register", 33 | "pretest:watch": "run-s -s build", 34 | "test:watch": "mocha -r @nomiclabs/buidler/register --watch-files '**/*.js,**/*.sol' --watch" 35 | }, 36 | "commitlint": { 37 | "extends": [ 38 | "@commitlint/config-conventional" 39 | ] 40 | }, 41 | "husky": { 42 | "hooks": { 43 | "pre-commit": "yarn lint && yarn lint:sol", 44 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 45 | } 46 | }, 47 | "devDependencies": { 48 | "@kleros/erc-792": "^8.0.0", 49 | "@nomiclabs/buidler": "^1.4.7", 50 | "@nomiclabs/buidler-ethers": "^2.0.0", 51 | "@nomiclabs/buidler-waffle": "^2.1.0", 52 | "@nomiclabs/buidler-web3": "^1.3.4", 53 | "@openzeppelin/test-helpers": "^0.5.6", 54 | "chai": "^4.2.0", 55 | "coveralls": "^3.0.2", 56 | "ethereum-waffle": "^3.1.0", 57 | "ethers": "^5.0.14", 58 | "ganache-cli": "^6.3.0", 59 | "husky": "^4.3.0", 60 | "npm-run-all": "^4.1.5", 61 | "pify": "^4.0.1", 62 | "prettier": "^2.1.2", 63 | "prettier-plugin-solidity": "^1.0.0-beta.10", 64 | "solhint": "^3.2.2", 65 | "solhint-plugin-prettier": "^0.0.5", 66 | "standard-version": "^4.4.0", 67 | "web3": "^1.3.0" 68 | }, 69 | "dependencies": { 70 | "@kleros/kleros": "^0.1.2", 71 | "@openzeppelin/contracts": "^4.8.2" 72 | }, 73 | "volta": { 74 | "node": "12.22.12" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /scripts/deploy_test.js: -------------------------------------------------------------------------------- 1 | // In order to deploy to Goerli, first make sure to add the right IDs in the builder.config.js file 2 | 3 | const { readArtifact } = require("@nomiclabs/buidler/plugins"); 4 | 5 | async function main() { 6 | const [deployer] = await ethers.getSigners(); 7 | 8 | console.log("Deploying contracts with the account:", await deployer.getAddress()); 9 | 10 | console.log("Account balance:", (await deployer.getBalance()).toString()); 11 | 12 | const contractArtifact = await readArtifact( 13 | "./artifacts/0.7.x", 14 | "MultipleArbitrableTransactionWithAppeals", 15 | ); 16 | const MultipleArbitrableTransaction = await ethers.getContractFactory( 17 | contractArtifact.abi, 18 | contractArtifact.bytecode, 19 | ); 20 | const contract = await MultipleArbitrableTransaction.deploy( 21 | "0xF2bD5519C747ADbf115F0b682897E09e51042964", 22 | "0x85", 23 | 100, 24 | 5000, 25 | 2000, 26 | 8000, 27 | ); 28 | await contract.deployed(); 29 | 30 | console.log("Contract address:", contract.address); 31 | } 32 | 33 | main() 34 | .then(() => process.exit(0)) 35 | .catch(error => { 36 | console.error(error); 37 | process.exit(1); 38 | }); 39 | -------------------------------------------------------------------------------- /src/entities/dispute-ruling.js: -------------------------------------------------------------------------------- 1 | const DisputeRuling = { 2 | RefusedToRule: 0, 3 | Sender: 1, 4 | Receiver: 2, 5 | }; 6 | 7 | module.exports = DisputeRuling; 8 | -------------------------------------------------------------------------------- /src/entities/transaction-party.js: -------------------------------------------------------------------------------- 1 | const TransactionParty = { 2 | None: 0, 3 | Sender: 1, 4 | Receiver: 2, 5 | }; 6 | 7 | module.exports = TransactionParty; 8 | -------------------------------------------------------------------------------- /src/entities/transaction-status.js: -------------------------------------------------------------------------------- 1 | const TransactionStatus = { 2 | NoDispute: 0, 3 | WaitingSettlementSender: 1, 4 | WaitingSettlementReceiver: 2, 5 | WaitingSender: 3, 6 | WaitingReceiver: 4, 7 | DisputeCreated: 5, 8 | Resolved: 6, 9 | }; 10 | 11 | module.exports = TransactionStatus; 12 | -------------------------------------------------------------------------------- /src/test-helpers.js: -------------------------------------------------------------------------------- 1 | const { time } = require("@openzeppelin/test-helpers"); 2 | 3 | /** 4 | * Get randomInt 5 | * @param {number} max Max integer. 6 | * @returns {number} Random integer in the range (0, max]. 7 | */ 8 | function randomInt(max) { 9 | return Math.ceil(Math.random() * max); 10 | } 11 | 12 | /** 13 | * getEmittedEvent 14 | * @param {string} eventName Name of the expected event. 15 | * @param {Promise} receipt Transaction promise. 16 | * @returns {object} Event data. 17 | */ 18 | function getEmittedEvent(eventName, receipt) { 19 | return receipt.events.find(({ event }) => event === eventName); 20 | } 21 | 22 | /** 23 | * Get latest time 24 | * @returns {number} Latest time. 25 | */ 26 | async function latestTime() { 27 | return Number(await time.latest()); 28 | } 29 | 30 | /** 31 | * Increase time by secondsPassed seconds. 32 | * @param {number} secondsPassed Time delta in seconds. 33 | * @returns {number} New current time. 34 | */ 35 | async function increaseTime(secondsPassed) { 36 | return time.increase(secondsPassed); 37 | } 38 | 39 | module.exports = { 40 | randomInt, 41 | getEmittedEvent, 42 | latestTime, 43 | increaseTime, 44 | }; 45 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | ### Tests 2 | 3 | The tests assumes you have at least the version 8.6 of node.js. 4 | If it's not the case you can get it there: https://nodejs.org/en/ 5 | Note that you'll have to reinstall ethereum js: npm install -g ethereumjs-testrpc 6 | -------------------------------------------------------------------------------- /test/multiple-arbitrable-token-transaction-with-appeals.gas-cost.js: -------------------------------------------------------------------------------- 1 | const { ethers } = require("@nomiclabs/buidler"); 2 | const { readArtifact } = require("@nomiclabs/buidler/plugins"); 3 | const { solidity } = require("ethereum-waffle"); 4 | const { use, expect } = require("chai"); 5 | 6 | const { getEmittedEvent, increaseTime } = require("../src/test-helpers"); 7 | const TransactionStatus = require("../src/entities/transaction-status"); 8 | const TransactionParty = require("../src/entities/transaction-party"); 9 | const DisputeRuling = require("../src/entities/dispute-ruling"); 10 | 11 | use(solidity); 12 | 13 | describe("MultipleArbitrableTokenTransactionWithAppeals contract", async () => { 14 | const arbitrationFee = 20; 15 | const arbitratorExtraData = "0x85"; 16 | const appealTimeout = 100; 17 | const feeTimeout = 100; 18 | const settlementTimeout = 100; 19 | const timeoutPayment = 100; 20 | const amount = 1000; 21 | const sharedMultiplier = 5000; 22 | const winnerMultiplier = 2000; 23 | const loserMultiplier = 8000; 24 | const metaEvidenceUri = "https://kleros.io"; 25 | 26 | let arbitrator; 27 | let sender; 28 | let receiver; 29 | let other; 30 | let crowdfunder1; 31 | let crowdfunder2; 32 | 33 | let senderAddress; 34 | let receiverAddress; 35 | 36 | let contractArtifact; 37 | let contract; 38 | let MULTIPLIER_DIVISOR; 39 | let token; 40 | 41 | beforeEach("Setup contracts", async () => { 42 | [_governor, sender, receiver, other, crowdfunder1, crowdfunder2] = await ethers.getSigners(); 43 | senderAddress = await sender.getAddress(); 44 | receiverAddress = await receiver.getAddress(); 45 | 46 | const arbitratorArtifact = await readArtifact( 47 | "./artifacts", 48 | "EnhancedAppealableArbitrator", 49 | ); 50 | const Arbitrator = await ethers.getContractFactory( 51 | arbitratorArtifact.abi, 52 | arbitratorArtifact.bytecode, 53 | ); 54 | arbitrator = await Arbitrator.deploy( 55 | String(arbitrationFee), 56 | ethers.constants.AddressZero, 57 | arbitratorExtraData, 58 | appealTimeout, 59 | ); 60 | await arbitrator.deployed(); 61 | // Make appeals go to the same arbitrator 62 | await arbitrator.changeArbitrator(arbitrator.address); 63 | 64 | const tokenArtifact = await readArtifact("./artifacts", "ERC20Mock"); 65 | const ERC20Token = await ethers.getContractFactory(tokenArtifact.abi, tokenArtifact.bytecode); 66 | token = await ERC20Token.deploy(senderAddress, amount * 10, "Pinakion", "PNK"); // (initial account, initial balance) 67 | await token.deployed(); 68 | 69 | contractArtifact = await readArtifact( 70 | "./artifacts", 71 | "MultipleArbitrableTokenTransactionWithAppeals", 72 | ); 73 | const MultipleArbitrableTransaction = await ethers.getContractFactory( 74 | contractArtifact.abi, 75 | contractArtifact.bytecode, 76 | ); 77 | contract = await MultipleArbitrableTransaction.deploy( 78 | arbitrator.address, 79 | arbitratorExtraData, 80 | feeTimeout, 81 | settlementTimeout, 82 | sharedMultiplier, 83 | winnerMultiplier, 84 | loserMultiplier, 85 | ); 86 | await contract.deployed(); 87 | 88 | // Gas estimations vary if address doesn't have any amount of tokens, so let's initialize it. 89 | const mintTx = await token.mint(receiverAddress, amount * 10); 90 | await mintTx.wait(); 91 | 92 | const approveTx = await token.connect(sender).approve(contract.address, amount * 2); 93 | await approveTx.wait(); 94 | // The first transaction is more expensive, because the hashes array is empty. Skip it to estimate gas costs on normal conditions. 95 | await createTransactionHelper(amount); 96 | 97 | MULTIPLIER_DIVISOR = await contract.MULTIPLIER_DIVISOR(); 98 | }); 99 | describe("Bytecode size estimations", () => { 100 | it("Should be smaller than the maximum allowed (24k)", async () => { 101 | const bytecode = contractArtifact.bytecode; 102 | const deployed = contractArtifact.deployedBytecode; 103 | const sizeOfB = bytecode.length / 2; 104 | const sizeOfD = deployed.length / 2; 105 | console.log("\tsize of bytecode in bytes = ", sizeOfB); 106 | console.log("\tsize of deployed in bytes = ", sizeOfD); 107 | expect(sizeOfD).to.be.lessThan(24576); 108 | }); 109 | }); 110 | 111 | describe("Gas costs estimations for single calls", () => { 112 | it("Estimate gas cost when creating transaction.", async () => { 113 | const metaEvidence = metaEvidenceUri; 114 | 115 | const tx = await contract 116 | .connect(sender) 117 | .createTransaction(amount, token.address, timeoutPayment, receiverAddress, metaEvidence); 118 | const receipt = await tx.wait(); 119 | 120 | console.log(""); 121 | console.log("\tGas used by createTransaction(): " + parseInt(receipt.gasUsed)); 122 | }); 123 | 124 | it("Estimate gas cost when reimbursing the sender.", async () => { 125 | const [_receipt, transactionId, transaction] = await createTransactionHelper(amount); 126 | 127 | const reimburseTx = await contract 128 | .connect(receiver) 129 | .reimburse(transactionId, transaction, amount); 130 | const reimburseReceipt = await reimburseTx.wait(); 131 | 132 | console.log(""); 133 | console.log("\tGas used by reimburse(): " + parseInt(reimburseReceipt.gasUsed)); 134 | }); 135 | 136 | it("Estimate gas cost when paying the receiver.", async () => { 137 | const [_receipt, transactionId, transaction] = await createTransactionHelper(amount); 138 | 139 | const payTx = await contract.connect(sender).pay(transactionId, transaction, amount); 140 | const payReceipt = await payTx.wait(); 141 | 142 | console.log(""); 143 | console.log("\tGas used by pay(): " + parseInt(payReceipt.gasUsed)); 144 | }); 145 | 146 | it("Estimate gas cost when executing the a transaction.", async () => { 147 | const [_receipt, transactionId, transaction] = await createTransactionHelper(amount); 148 | 149 | await increaseTime(timeoutPayment); 150 | 151 | // Anyone should be allowed to execute the transaction. 152 | const executeTx = await contract 153 | .connect(other) 154 | .executeTransaction(transactionId, transaction); 155 | const executeReceipt = await executeTx.wait(); 156 | 157 | console.log(""); 158 | console.log("\tGas used by executeTransaction(): " + parseInt(executeReceipt.gasUsed)); 159 | }); 160 | 161 | it("Estimate gas cost when paying fee (first party calling).", async () => { 162 | const [_receipt, transactionId, transaction] = await createTransactionHelper(amount); 163 | 164 | const senderSettlementTx = await contract 165 | .connect(receiver) 166 | .proposeSettlement(transactionId, transaction, transaction.amount / 2); 167 | const senderSettltementReceipt = await senderSettlementTx.wait(); 168 | 169 | await increaseTime(100); 170 | 171 | const [settlementTransactionId, settlementTransaction] = getEmittedEvent( 172 | "TransactionStateUpdated", 173 | senderSettltementReceipt, 174 | ).args; 175 | 176 | const senderFeePromise = contract 177 | .connect(sender) 178 | .payArbitrationFeeBySender(settlementTransactionId, settlementTransaction, { 179 | value: arbitrationFee, 180 | }); 181 | const senderFeeTx = await senderFeePromise; 182 | const senderFeeReceipt = await senderFeeTx.wait(); 183 | 184 | console.log(""); 185 | console.log( 186 | "\tGas used by payArbitrationFeeBySender(): " + parseInt(senderFeeReceipt.gasUsed), 187 | ); 188 | }); 189 | 190 | it("Estimate gas cost when paying fee (second party calling) and creating dispute.", async () => { 191 | const [_receipt, transactionId, transaction] = await createTransactionHelper(amount); 192 | 193 | const senderSettlementTx = await contract 194 | .connect(receiver) 195 | .proposeSettlement(transactionId, transaction, transaction.amount / 2); 196 | const senderSettltementReceipt = await senderSettlementTx.wait(); 197 | 198 | await increaseTime(100); 199 | 200 | const [settlementTransactionId, settlementTransaction] = getEmittedEvent( 201 | "TransactionStateUpdated", 202 | senderSettltementReceipt, 203 | ).args; 204 | 205 | const receiverTxPromise = contract 206 | .connect(receiver) 207 | .payArbitrationFeeByReceiver(settlementTransactionId, settlementTransaction, { 208 | value: arbitrationFee, 209 | }); 210 | const receiverFeeTx = await receiverTxPromise; 211 | const receiverFeeReceipt = await receiverFeeTx.wait(); 212 | const [_receiverFeeTransactionId, receiverFeeTransaction] = getEmittedEvent( 213 | "TransactionStateUpdated", 214 | receiverFeeReceipt, 215 | ).args; 216 | 217 | const senderFeePromise = contract 218 | .connect(sender) 219 | .payArbitrationFeeBySender(transactionId, receiverFeeTransaction, { 220 | value: arbitrationFee, 221 | }); 222 | const senderFeeTx = await senderFeePromise; 223 | const senderFeeReceipt = await senderFeeTx.wait(); 224 | 225 | console.log(""); 226 | console.log( 227 | "\tGas used by payArbitrationFeeBySender(): " + parseInt(senderFeeReceipt.gasUsed), 228 | ); 229 | }); 230 | 231 | it("Estimate gas cost when timing out.", async () => { 232 | const [_receipt, transactionId, transaction] = await createTransactionHelper(amount); 233 | 234 | const senderSettlementTx = await contract 235 | .connect(receiver) 236 | .proposeSettlement(transactionId, transaction, transaction.amount / 2); 237 | const senderSettltementReceipt = await senderSettlementTx.wait(); 238 | 239 | await increaseTime(100); 240 | 241 | const [settlementTransactionId, settlementTransaction] = getEmittedEvent( 242 | "TransactionStateUpdated", 243 | senderSettltementReceipt, 244 | ).args; 245 | 246 | const senderFeePromise = contract 247 | .connect(sender) 248 | .payArbitrationFeeBySender(settlementTransactionId, settlementTransaction, { 249 | value: arbitrationFee, 250 | }); 251 | const senderFeeTx = await senderFeePromise; 252 | const senderFeeReceipt = await senderFeeTx.wait(); 253 | 254 | const [senderFeeTransactionId, senderFeeTransaction] = getEmittedEvent( 255 | "TransactionStateUpdated", 256 | senderFeeReceipt, 257 | ).args; 258 | 259 | // feeTimeout for receiver passes and sender gets to claim amount and his fee. 260 | await increaseTime(feeTimeout + 1); 261 | // Anyone can execute the timeout 262 | const timeoutTx = await contract 263 | .connect(other) 264 | .timeOutBySender(senderFeeTransactionId, senderFeeTransaction); 265 | const timeoutReceipt = await timeoutTx.wait(); 266 | 267 | console.log(""); 268 | console.log("\tGas used by timeOutBySender(): " + parseInt(timeoutReceipt.gasUsed)); 269 | }); 270 | 271 | it("Estimate gas cost when executing a ruled dispute.", async () => { 272 | const [_receipt, transactionId, transaction] = await createTransactionHelper(amount); 273 | const [disputeID, _disputeTransactionId, disputeTransaction] = await createDisputeHelper( 274 | transactionId, 275 | transaction, 276 | ); 277 | 278 | await giveFinalRulingHelper(disputeID, DisputeRuling.Sender); 279 | 280 | const txPromise = contract.connect(other).executeRuling(transactionId, disputeTransaction); 281 | const tx = await txPromise; 282 | const receipt = await tx.wait(); 283 | 284 | console.log(""); 285 | console.log("\tGas used by executeRuling(): " + parseInt(receipt.gasUsed)); 286 | }); 287 | 288 | it("Estimate gas cost when executing a ruled dispute where jurors refused to rule.", async () => { 289 | const [_receipt, transactionId, transaction] = await createTransactionHelper(amount); 290 | const [disputeID, _disputeTransactionId, disputeTransaction] = await createDisputeHelper( 291 | transactionId, 292 | transaction, 293 | ); 294 | 295 | await giveFinalRulingHelper(disputeID, DisputeRuling.RefusedToRule); 296 | 297 | const txPromise = contract.connect(other).executeRuling(transactionId, disputeTransaction); 298 | const tx = await txPromise; 299 | const receipt = await tx.wait(); 300 | 301 | console.log(""); 302 | console.log("\tGas used by executeRuling(): " + parseInt(receipt.gasUsed)); 303 | }); 304 | 305 | it("Estimate gas cost when submitting evidence.", async () => { 306 | const [_receipt, transactionId, transaction] = await createTransactionHelper(amount); 307 | 308 | const txPromise = contract 309 | .connect(sender) 310 | .submitEvidence(transactionId, transaction, "ipfs:/evidence_001"); 311 | const tx = await txPromise; 312 | const receipt = await tx.wait(); 313 | 314 | console.log(""); 315 | console.log("\tGas used by submitEvidence(): " + parseInt(receipt.gasUsed)); 316 | }); 317 | 318 | it("Estimate gas cost when appealing one side (full funding).", async () => { 319 | const loserAppealFee = 320 | arbitrationFee + (arbitrationFee * loserMultiplier) / MULTIPLIER_DIVISOR; 321 | 322 | const [_receipt, transactionId, transaction] = await createTransactionHelper(amount); 323 | const [disputeID, _disputeTransactionId, disputeTransaction] = await createDisputeHelper( 324 | transactionId, 325 | transaction, 326 | ); 327 | 328 | await giveRulingHelper(disputeID, DisputeRuling.Sender); 329 | 330 | // Fully fund the loser side 331 | const txPromise = contract 332 | .connect(receiver) 333 | .fundAppeal(transactionId, disputeTransaction, TransactionParty.Receiver, { 334 | value: loserAppealFee, 335 | }); 336 | const tx = await txPromise; 337 | const receipt = await tx.wait(); 338 | 339 | console.log(""); 340 | console.log("\tGas used by fundAppeal(): " + parseInt(receipt.gasUsed)); 341 | }); 342 | 343 | it("Estimate gas cost when appealing one side (partial funding).", async () => { 344 | const loserAppealFee = 345 | arbitrationFee + (arbitrationFee * loserMultiplier) / MULTIPLIER_DIVISOR; 346 | 347 | const [_receipt, transactionId, transaction] = await createTransactionHelper(amount); 348 | const [disputeID, _disputeTransactionId, disputeTransaction] = await createDisputeHelper( 349 | transactionId, 350 | transaction, 351 | ); 352 | 353 | await giveRulingHelper(disputeID, DisputeRuling.Sender); 354 | 355 | // Fully fund the loser side 356 | const txPromise = contract 357 | .connect(crowdfunder1) 358 | .fundAppeal(transactionId, disputeTransaction, TransactionParty.Receiver, { 359 | value: loserAppealFee / 2, 360 | }); 361 | const tx = await txPromise; 362 | const receipt = await tx.wait(); 363 | 364 | console.log(""); 365 | console.log("\tGas used by fundAppeal(): " + parseInt(receipt.gasUsed)); 366 | }); 367 | 368 | it("Estimate gas cost when appealing one side (full funding) and creating new round.", async () => { 369 | const loserAppealFee = 370 | arbitrationFee + (arbitrationFee * loserMultiplier) / MULTIPLIER_DIVISOR; 371 | const winnerAppealFee = 372 | arbitrationFee + (arbitrationFee * winnerMultiplier) / MULTIPLIER_DIVISOR; 373 | 374 | const [_receipt, transactionId, transaction] = await createTransactionHelper(amount); 375 | const [disputeID, _disputeTransactionId, disputeTransaction] = await createDisputeHelper( 376 | transactionId, 377 | transaction, 378 | ); 379 | 380 | await giveRulingHelper(disputeID, DisputeRuling.Sender); 381 | 382 | // Fully fund the loser side 383 | await fundAppealHelper( 384 | transactionId, 385 | disputeTransaction, 386 | receiver, 387 | loserAppealFee, 388 | TransactionParty.Receiver, 389 | ); 390 | const [_txPromise, _tx, receipt] = await fundAppealHelper( 391 | transactionId, 392 | disputeTransaction, 393 | sender, 394 | winnerAppealFee, 395 | TransactionParty.Sender, 396 | ); 397 | 398 | console.log(""); 399 | console.log("\tGas used by fundAppeal(): " + parseInt(receipt.gasUsed)); 400 | }); 401 | 402 | it("Estimate gas cost when withdrawing one round (winner side).", async () => { 403 | const loserAppealFee = 404 | arbitrationFee + (arbitrationFee * loserMultiplier) / MULTIPLIER_DIVISOR; 405 | const winnerAppealFee = 406 | arbitrationFee + (arbitrationFee * winnerMultiplier) / MULTIPLIER_DIVISOR; 407 | 408 | const [_receipt, transactionId, transaction] = await createTransactionHelper(amount); 409 | const [disputeID, _disputeTransactionId, disputeTransaction] = await createDisputeHelper( 410 | transactionId, 411 | transaction, 412 | ); 413 | 414 | await giveRulingHelper(disputeID, DisputeRuling.Sender); 415 | 416 | // Fully fund the loser side 417 | await fundAppealHelper( 418 | transactionId, 419 | disputeTransaction, 420 | receiver, 421 | loserAppealFee, 422 | TransactionParty.Receiver, 423 | ); 424 | await fundAppealHelper( 425 | transactionId, 426 | disputeTransaction, 427 | sender, 428 | winnerAppealFee, 429 | TransactionParty.Sender, 430 | ); 431 | 432 | // Give and execute final ruling 433 | const appealDisputeID = await arbitrator.getAppealDisputeID(disputeID); 434 | await giveFinalRulingHelper(appealDisputeID, DisputeRuling.Sender, disputeID); 435 | const [_ruleTransactionId, ruleTransaction] = await executeRulingHelper( 436 | transactionId, 437 | disputeTransaction, 438 | other, 439 | ); 440 | 441 | const [_txPromise, _tx, receipt] = await withdrawHelper( 442 | senderAddress, 443 | transactionId, 444 | ruleTransaction, 445 | 0, 446 | sender, 447 | ); 448 | 449 | console.log(""); 450 | console.log("\tGas used by withdrawFeesAndRewards(): " + parseInt(receipt.gasUsed)); 451 | }); 452 | 453 | it("Estimate gas cost when batch-withdrawing 5 rounds (winner side).", async () => { 454 | const loserAppealFee = 455 | arbitrationFee + (arbitrationFee * loserMultiplier) / MULTIPLIER_DIVISOR; 456 | const winnerAppealFee = 457 | arbitrationFee + (arbitrationFee * winnerMultiplier) / MULTIPLIER_DIVISOR; 458 | const roundsLength = 5; 459 | 460 | const [_receipt, transactionId, transaction] = await createTransactionHelper(amount); 461 | const [disputeID, _disputeTransactionId, disputeTransaction] = await createDisputeHelper( 462 | transactionId, 463 | transaction, 464 | ); 465 | 466 | let roundDisputeID; 467 | roundDisputeID = disputeID; 468 | for (var roundI = 0; roundI < roundsLength; roundI += 1) { 469 | await giveRulingHelper(roundDisputeID, DisputeRuling.Sender); 470 | // Fully fund both sides 471 | await fundAppealHelper( 472 | transactionId, 473 | disputeTransaction, 474 | crowdfunder1, 475 | loserAppealFee, 476 | TransactionParty.Receiver, 477 | ); 478 | await fundAppealHelper( 479 | transactionId, 480 | disputeTransaction, 481 | crowdfunder2, 482 | winnerAppealFee, 483 | TransactionParty.Sender, 484 | ); 485 | roundDisputeID = await arbitrator.getAppealDisputeID(disputeID); 486 | } 487 | 488 | // Give and execute final ruling 489 | await giveFinalRulingHelper(roundDisputeID, DisputeRuling.Sender, disputeID); 490 | const [_ruleTransactionId, ruleTransaction] = await executeRulingHelper( 491 | transactionId, 492 | disputeTransaction, 493 | other, 494 | ); 495 | 496 | // Batch-withdraw (checking if _cursor and _count arguments are working as expected). 497 | const txPromise = contract 498 | .connect(other) 499 | .batchRoundWithdraw(await crowdfunder2.getAddress(), transactionId, ruleTransaction, 0, 0); 500 | const tx = await txPromise; 501 | const receipt = await tx.wait(); 502 | 503 | console.log(""); 504 | console.log("\tGas used by batchRoundWithdraw(): " + parseInt(receipt.gasUsed)); 505 | }); 506 | }); 507 | 508 | /** 509 | * Creates a transaction by sender to receiver. 510 | * @param {number} _amount Amount in wei. 511 | * @returns {Array} Tx data. 512 | */ 513 | async function createTransactionHelper(_amount) { 514 | const metaEvidence = metaEvidenceUri; 515 | 516 | const tx = await contract 517 | .connect(sender) 518 | .createTransaction(_amount, token.address, timeoutPayment, receiverAddress, metaEvidence); 519 | const receipt = await tx.wait(); 520 | const [transactionId, transaction] = getEmittedEvent("TransactionStateUpdated", receipt).args; 521 | 522 | return [receipt, transactionId, transaction]; 523 | } 524 | 525 | /** 526 | * Make both sides pay arbitration fees. The transaction should have been previosuly created. 527 | * @param {number} _transactionId Id of the transaction. 528 | * @param {object} _transaction Current transaction object. 529 | * @param {number} fee Appeal round from which to withdraw the rewards. 530 | * @returns {Array} Tx data. 531 | */ 532 | async function createDisputeHelper(_transactionId, _transaction, fee = arbitrationFee) { 533 | // Pay fees, create dispute and validate events. 534 | const receiverSettlementTx = await contract 535 | .connect(receiver) 536 | .proposeSettlement(_transactionId, _transaction, _transaction.amount); 537 | const receiverSettltementReceipt = await receiverSettlementTx.wait(); 538 | 539 | await increaseTime(100); 540 | 541 | const [settlementTransactionId, settlementTransaction] = getEmittedEvent( 542 | "TransactionStateUpdated", 543 | receiverSettltementReceipt, 544 | ).args; 545 | 546 | const receiverTxPromise = contract 547 | .connect(receiver) 548 | .payArbitrationFeeByReceiver(settlementTransactionId, settlementTransaction, { 549 | value: fee, 550 | }); 551 | const receiverFeeTx = await receiverTxPromise; 552 | const receiverFeeReceipt = await receiverFeeTx.wait(); 553 | expect(receiverTxPromise) 554 | .to.emit(contract, "HasToPayFee") 555 | .withArgs(_transactionId, TransactionParty.Sender); 556 | const [receiverFeeTransactionId, receiverFeeTransaction] = getEmittedEvent( 557 | "TransactionStateUpdated", 558 | receiverFeeReceipt, 559 | ).args; 560 | const txPromise = contract 561 | .connect(sender) 562 | .payArbitrationFeeBySender(receiverFeeTransactionId, receiverFeeTransaction, { 563 | value: fee, 564 | }); 565 | const senderFeeTx = await txPromise; 566 | const senderFeeReceipt = await senderFeeTx.wait(); 567 | const [senderFeeTransactionId, senderFeeTransaction] = getEmittedEvent( 568 | "TransactionStateUpdated", 569 | senderFeeReceipt, 570 | ).args; 571 | expect(txPromise) 572 | .to.emit(contract, "Dispute") 573 | .withArgs( 574 | arbitrator.address, 575 | senderFeeTransaction.disputeID, 576 | senderFeeTransactionId, 577 | senderFeeTransactionId, 578 | ); 579 | expect(senderFeeTransaction.status).to.equal( 580 | TransactionStatus.DisputeCreated, 581 | "Invalid transaction status", 582 | ); 583 | return [senderFeeTransaction.disputeID, senderFeeTransactionId, senderFeeTransaction]; 584 | } 585 | 586 | /** 587 | * Give ruling (not final). 588 | * @param {number} disputeID dispute ID. 589 | * @param {number} ruling Ruling: None, Sender or Receiver. 590 | * @returns {Array} Tx data. 591 | */ 592 | async function giveRulingHelper(disputeID, ruling) { 593 | // Notice that rule() function is not called by the arbitrator, because the dispute is appealable. 594 | const txPromise = arbitrator.giveRuling(disputeID, ruling); 595 | const tx = await txPromise; 596 | const receipt = await tx.wait(); 597 | 598 | return [txPromise, tx, receipt]; 599 | } 600 | 601 | /** 602 | * Give final ruling and enforce it. 603 | * @param {number} disputeID dispute ID. 604 | * @param {number} ruling Ruling: None, Sender or Receiver. 605 | * @param {number} transactionDisputeId Initial dispute ID. 606 | * @returns {Array} Random integer in the range (0, max]. 607 | */ 608 | async function giveFinalRulingHelper(disputeID, ruling, transactionDisputeId = disputeID) { 609 | const firstTx = await arbitrator.giveRuling(disputeID, ruling); 610 | await firstTx.wait(); 611 | 612 | await increaseTime(appealTimeout + 1); 613 | 614 | const txPromise = arbitrator.giveRuling(disputeID, ruling); 615 | const tx = await txPromise; 616 | const receipt = await tx.wait(); 617 | 618 | expect(txPromise) 619 | .to.emit(contract, "Ruling") 620 | .withArgs(arbitrator.address, transactionDisputeId, ruling); 621 | 622 | return [txPromise, tx, receipt]; 623 | } 624 | 625 | /** 626 | * Execute the final ruling. 627 | * @param {number} transactionId Id of the transaction. 628 | * @param {object} transaction Current transaction object. 629 | * @param {address} caller Can be anyone. 630 | * @returns {Array} Transaction ID and the updated object. 631 | */ 632 | async function executeRulingHelper(transactionId, transaction, caller) { 633 | const tx = await contract.connect(caller).executeRuling(transactionId, transaction); 634 | const receipt = await tx.wait(); 635 | const [newTransactionId, newTransaction] = getEmittedEvent( 636 | "TransactionStateUpdated", 637 | receipt, 638 | ).args; 639 | 640 | return [newTransactionId, newTransaction]; 641 | } 642 | 643 | /** 644 | * Fund new appeal round. 645 | * @param {number} transactionId Id of the transaction. 646 | * @param {object} transaction Current transaction object. 647 | * @param {address} caller Can be anyone. 648 | * @param {number} contribution Contribution amount in wei. 649 | * @param {number} side Side to contribute to: Sender or Receiver. 650 | * @returns {Array} Tx data. 651 | */ 652 | async function fundAppealHelper(transactionId, transaction, caller, contribution, side) { 653 | const txPromise = contract 654 | .connect(caller) 655 | .fundAppeal(transactionId, transaction, side, { value: contribution }); 656 | const tx = await txPromise; 657 | const receipt = await tx.wait(); 658 | 659 | return [txPromise, tx, receipt]; 660 | } 661 | 662 | /** 663 | * Withdraw rewards to beneficiary. 664 | * @param {address} beneficiary Address of the round contributor. 665 | * @param {number} transactionId Id of the transaction. 666 | * @param {object} transaction Current transaction object. 667 | * @param {number} round Appeal round from which to withdraw the rewards. 668 | * @param {address} caller Can be anyone. 669 | * @returns {Array} Tx data. 670 | */ 671 | async function withdrawHelper(beneficiary, transactionId, transaction, round, caller) { 672 | const txPromise = contract 673 | .connect(caller) 674 | .withdrawFeesAndRewards(beneficiary, transactionId, transaction, round); 675 | const tx = await txPromise; 676 | const receipt = await tx.wait(); 677 | 678 | return [txPromise, tx, receipt]; 679 | } 680 | }); 681 | -------------------------------------------------------------------------------- /test/multiple-arbitrable-transaction-with-appeals.gas-cost.js: -------------------------------------------------------------------------------- 1 | const { ethers } = require("@nomiclabs/buidler"); 2 | const { readArtifact } = require("@nomiclabs/buidler/plugins"); 3 | const { solidity } = require("ethereum-waffle"); 4 | const { use, expect } = require("chai"); 5 | 6 | const { getEmittedEvent, increaseTime } = require("../src/test-helpers"); 7 | const TransactionStatus = require("../src/entities/transaction-status"); 8 | const TransactionParty = require("../src/entities/transaction-party"); 9 | const DisputeRuling = require("../src/entities/dispute-ruling"); 10 | 11 | use(solidity); 12 | 13 | describe("MultipleArbitrableTransactionWithAppeals contract", async () => { 14 | const arbitrationFee = 20; 15 | const arbitratorExtraData = "0x85"; 16 | const appealTimeout = 100; 17 | const feeTimeout = 100; 18 | const settlementTimeout = 100; 19 | const timeoutPayment = 100; 20 | const amount = 1000; 21 | const sharedMultiplier = 5000; 22 | const winnerMultiplier = 2000; 23 | const loserMultiplier = 8000; 24 | const metaEvidenceUri = "https://kleros.io"; 25 | 26 | let arbitrator; 27 | let sender; 28 | let receiver; 29 | let other; 30 | let crowdfunder1; 31 | let crowdfunder2; 32 | 33 | let contract; 34 | let MULTIPLIER_DIVISOR; 35 | 36 | let contractArtifact; 37 | 38 | beforeEach("Setup contracts", async () => { 39 | [_governor, sender, receiver, other, crowdfunder1, crowdfunder2] = await ethers.getSigners(); 40 | 41 | const arbitratorArtifact = await readArtifact( 42 | "./artifacts", 43 | "EnhancedAppealableArbitrator", 44 | ); 45 | const Arbitrator = await ethers.getContractFactory( 46 | arbitratorArtifact.abi, 47 | arbitratorArtifact.bytecode, 48 | ); 49 | arbitrator = await Arbitrator.deploy( 50 | String(arbitrationFee), 51 | ethers.constants.AddressZero, 52 | arbitratorExtraData, 53 | appealTimeout, 54 | ); 55 | await arbitrator.deployed(); 56 | // Make appeals go to the same arbitrator 57 | await arbitrator.changeArbitrator(arbitrator.address); 58 | 59 | contractArtifact = await readArtifact( 60 | "./artifacts", 61 | "MultipleArbitrableTransactionWithAppeals", 62 | ); 63 | const MultipleArbitrableTransaction = await ethers.getContractFactory( 64 | contractArtifact.abi, 65 | contractArtifact.bytecode, 66 | ); 67 | contract = await MultipleArbitrableTransaction.deploy( 68 | arbitrator.address, 69 | arbitratorExtraData, 70 | feeTimeout, 71 | settlementTimeout, 72 | sharedMultiplier, 73 | winnerMultiplier, 74 | loserMultiplier, 75 | ); 76 | await contract.deployed(); 77 | 78 | // The first transaction is more expensive, because the hashes array is empty. Skip it to estimate gas costs on normal conditions. 79 | await createTransactionHelper(amount); 80 | 81 | MULTIPLIER_DIVISOR = await contract.MULTIPLIER_DIVISOR(); 82 | }); 83 | 84 | describe("Bytecode size estimations", () => { 85 | it("Should be smaller than the maximum allowed (24k)", async () => { 86 | const bytecode = contractArtifact.bytecode; 87 | const deployed = contractArtifact.deployedBytecode; 88 | const sizeOfB = bytecode.length / 2; 89 | const sizeOfD = deployed.length / 2; 90 | console.log("\tsize of bytecode in bytes = ", sizeOfB); 91 | console.log("\tsize of deployed in bytes = ", sizeOfD); 92 | expect(sizeOfD).to.be.lessThan(24576); 93 | }); 94 | }); 95 | 96 | describe("Gas costs estimations for single calls", () => { 97 | it("Estimate gas cost when creating transaction.", async () => { 98 | const receiverAddress = await receiver.getAddress(); 99 | const metaEvidence = metaEvidenceUri; 100 | 101 | const tx = await contract 102 | .connect(sender) 103 | .createTransaction(timeoutPayment, receiverAddress, metaEvidence, { 104 | value: amount, 105 | }); 106 | const receipt = await tx.wait(); 107 | 108 | console.log(""); 109 | console.log("\tGas used by createTransaction(): " + parseInt(receipt.gasUsed)); 110 | }); 111 | 112 | it("Estimate gas cost when reimbursing the sender.", async () => { 113 | const [_receipt, transactionId, transaction] = await createTransactionHelper(amount); 114 | 115 | const reimburseTx = await contract 116 | .connect(receiver) 117 | .reimburse(transactionId, transaction, amount); 118 | const reimburseReceipt = await reimburseTx.wait(); 119 | 120 | console.log(""); 121 | console.log("\tGas used by reimburse(): " + parseInt(reimburseReceipt.gasUsed)); 122 | }); 123 | 124 | it("Estimate gas cost when paying the receiver.", async () => { 125 | const [_receipt, transactionId, transaction] = await createTransactionHelper(amount); 126 | 127 | const payTx = await contract.connect(sender).pay(transactionId, transaction, amount); 128 | const payReceipt = await payTx.wait(); 129 | 130 | console.log(""); 131 | console.log("\tGas used by pay(): " + parseInt(payReceipt.gasUsed)); 132 | }); 133 | 134 | it("Estimate gas cost when executing the a transaction.", async () => { 135 | const [_receipt, transactionId, transaction] = await createTransactionHelper(amount); 136 | 137 | await increaseTime(timeoutPayment); 138 | 139 | // Anyone should be allowed to execute the transaction. 140 | const executeTx = await contract 141 | .connect(other) 142 | .executeTransaction(transactionId, transaction); 143 | const executeReceipt = await executeTx.wait(); 144 | 145 | console.log(""); 146 | console.log("\tGas used by executeTransaction(): " + parseInt(executeReceipt.gasUsed)); 147 | }); 148 | 149 | it("Estimate gas cost when paying fee (first party calling).", async () => { 150 | const [_receipt, transactionId, transaction] = await createTransactionHelper(amount); 151 | 152 | const senderSettlementTx = await contract 153 | .connect(receiver) 154 | .proposeSettlement(transactionId, transaction, transaction.amount / 2); 155 | const senderSettltementReceipt = await senderSettlementTx.wait(); 156 | 157 | await increaseTime(100); 158 | 159 | const [settlementTransactionId, settlementTransaction] = getEmittedEvent( 160 | "TransactionStateUpdated", 161 | senderSettltementReceipt, 162 | ).args; 163 | 164 | const senderFeePromise = contract 165 | .connect(sender) 166 | .payArbitrationFeeBySender(settlementTransactionId, settlementTransaction, { 167 | value: arbitrationFee, 168 | }); 169 | const senderFeeTx = await senderFeePromise; 170 | const senderFeeReceipt = await senderFeeTx.wait(); 171 | 172 | console.log(""); 173 | console.log( 174 | "\tGas used by payArbitrationFeeBySender(): " + parseInt(senderFeeReceipt.gasUsed), 175 | ); 176 | }); 177 | 178 | it("Estimate gas cost when paying fee (second party calling) and creating dispute.", async () => { 179 | const [_receipt, transactionId, transaction] = await createTransactionHelper(amount); 180 | 181 | const senderSettlementTx = await contract 182 | .connect(receiver) 183 | .proposeSettlement(transactionId, transaction, transaction.amount / 2); 184 | const senderSettltementReceipt = await senderSettlementTx.wait(); 185 | 186 | await increaseTime(100); 187 | 188 | const [settlementTransactionId, settlementTransaction] = getEmittedEvent( 189 | "TransactionStateUpdated", 190 | senderSettltementReceipt, 191 | ).args; 192 | 193 | const receiverTxPromise = contract 194 | .connect(receiver) 195 | .payArbitrationFeeByReceiver(settlementTransactionId, settlementTransaction, { 196 | value: arbitrationFee, 197 | }); 198 | const receiverFeeTx = await receiverTxPromise; 199 | const receiverFeeReceipt = await receiverFeeTx.wait(); 200 | const [_receiverFeeTransactionId, receiverFeeTransaction] = getEmittedEvent( 201 | "TransactionStateUpdated", 202 | receiverFeeReceipt, 203 | ).args; 204 | 205 | const senderFeePromise = contract 206 | .connect(sender) 207 | .payArbitrationFeeBySender(transactionId, receiverFeeTransaction, { 208 | value: arbitrationFee, 209 | }); 210 | const senderFeeTx = await senderFeePromise; 211 | const senderFeeReceipt = await senderFeeTx.wait(); 212 | 213 | console.log(""); 214 | console.log( 215 | "\tGas used by payArbitrationFeeBySender(): " + parseInt(senderFeeReceipt.gasUsed), 216 | ); 217 | }); 218 | 219 | it("Estimate gas cost when timing out.", async () => { 220 | const [_receipt, transactionId, transaction] = await createTransactionHelper(amount); 221 | 222 | const senderSettlementTx = await contract 223 | .connect(receiver) 224 | .proposeSettlement(transactionId, transaction, transaction.amount / 2); 225 | const senderSettltementReceipt = await senderSettlementTx.wait(); 226 | 227 | await increaseTime(100); 228 | 229 | const [settlementTransactionId, settlementTransaction] = getEmittedEvent( 230 | "TransactionStateUpdated", 231 | senderSettltementReceipt, 232 | ).args; 233 | 234 | const senderFeePromise = contract 235 | .connect(sender) 236 | .payArbitrationFeeBySender(settlementTransactionId, settlementTransaction, { 237 | value: arbitrationFee, 238 | }); 239 | const senderFeeTx = await senderFeePromise; 240 | const senderFeeReceipt = await senderFeeTx.wait(); 241 | 242 | const [senderFeeTransactionId, senderFeeTransaction] = getEmittedEvent( 243 | "TransactionStateUpdated", 244 | senderFeeReceipt, 245 | ).args; 246 | 247 | // feeTimeout for receiver passes and sender gets to claim amount and his fee. 248 | await increaseTime(feeTimeout + 1); 249 | // Anyone can execute the timeout 250 | const timeoutTx = await contract 251 | .connect(other) 252 | .timeOutBySender(senderFeeTransactionId, senderFeeTransaction); 253 | const timeoutReceipt = await timeoutTx.wait(); 254 | 255 | console.log(""); 256 | console.log("\tGas used by timeOutBySender(): " + parseInt(timeoutReceipt.gasUsed)); 257 | }); 258 | 259 | it("Estimate gas cost when executing a ruled dispute.", async () => { 260 | const [_receipt, transactionId, transaction] = await createTransactionHelper(amount); 261 | const [disputeID, _disputeTransactionId, disputeTransaction] = await createDisputeHelper( 262 | transactionId, 263 | transaction, 264 | ); 265 | 266 | await giveFinalRulingHelper(disputeID, DisputeRuling.Sender); 267 | 268 | const txPromise = contract.connect(other).executeRuling(transactionId, disputeTransaction); 269 | const tx = await txPromise; 270 | const receipt = await tx.wait(); 271 | 272 | console.log(""); 273 | console.log("\tGas used by executeRuling(): " + parseInt(receipt.gasUsed)); 274 | }); 275 | 276 | it("Estimate gas cost when executing a ruled dispute where jurors refused to rule.", async () => { 277 | const [_receipt, transactionId, transaction] = await createTransactionHelper(amount); 278 | const [disputeID, _disputeTransactionId, disputeTransaction] = await createDisputeHelper( 279 | transactionId, 280 | transaction, 281 | ); 282 | 283 | await giveFinalRulingHelper(disputeID, DisputeRuling.RefusedToRule); 284 | 285 | const txPromise = contract.connect(other).executeRuling(transactionId, disputeTransaction); 286 | const tx = await txPromise; 287 | const receipt = await tx.wait(); 288 | 289 | console.log(""); 290 | console.log("\tGas used by executeRuling(): " + parseInt(receipt.gasUsed)); 291 | }); 292 | 293 | it("Estimate gas cost when submitting evidence.", async () => { 294 | const [_receipt, transactionId, transaction] = await createTransactionHelper(amount); 295 | 296 | const txPromise = contract 297 | .connect(sender) 298 | .submitEvidence(transactionId, transaction, "ipfs:/evidence_001"); 299 | const tx = await txPromise; 300 | const receipt = await tx.wait(); 301 | 302 | console.log(""); 303 | console.log("\tGas used by submitEvidence(): " + parseInt(receipt.gasUsed)); 304 | }); 305 | 306 | it("Estimate gas cost when appealing one side (full funding).", async () => { 307 | const loserAppealFee = 308 | arbitrationFee + (arbitrationFee * loserMultiplier) / MULTIPLIER_DIVISOR; 309 | 310 | const [_receipt, transactionId, transaction] = await createTransactionHelper(amount); 311 | const [disputeID, _disputeTransactionId, disputeTransaction] = await createDisputeHelper( 312 | transactionId, 313 | transaction, 314 | ); 315 | 316 | await giveRulingHelper(disputeID, DisputeRuling.Sender); 317 | 318 | // Fully fund the loser side 319 | const txPromise = contract 320 | .connect(receiver) 321 | .fundAppeal(transactionId, disputeTransaction, TransactionParty.Receiver, { 322 | value: loserAppealFee, 323 | }); 324 | const tx = await txPromise; 325 | const receipt = await tx.wait(); 326 | 327 | console.log(""); 328 | console.log("\tGas used by fundAppeal(): " + parseInt(receipt.gasUsed)); 329 | }); 330 | 331 | it("Estimate gas cost when appealing one side (partial funding).", async () => { 332 | const loserAppealFee = 333 | arbitrationFee + (arbitrationFee * loserMultiplier) / MULTIPLIER_DIVISOR; 334 | 335 | const [_receipt, transactionId, transaction] = await createTransactionHelper(amount); 336 | const [disputeID, _disputeTransactionId, disputeTransaction] = await createDisputeHelper( 337 | transactionId, 338 | transaction, 339 | ); 340 | 341 | await giveRulingHelper(disputeID, DisputeRuling.Sender); 342 | 343 | // Fully fund the loser side 344 | const txPromise = contract 345 | .connect(crowdfunder1) 346 | .fundAppeal(transactionId, disputeTransaction, TransactionParty.Receiver, { 347 | value: loserAppealFee / 2, 348 | }); 349 | const tx = await txPromise; 350 | const receipt = await tx.wait(); 351 | 352 | console.log(""); 353 | console.log("\tGas used by fundAppeal(): " + parseInt(receipt.gasUsed)); 354 | }); 355 | 356 | it("Estimate gas cost when appealing one side (full funding) and creating new round.", async () => { 357 | const loserAppealFee = 358 | arbitrationFee + (arbitrationFee * loserMultiplier) / MULTIPLIER_DIVISOR; 359 | const winnerAppealFee = 360 | arbitrationFee + (arbitrationFee * winnerMultiplier) / MULTIPLIER_DIVISOR; 361 | 362 | const [_receipt, transactionId, transaction] = await createTransactionHelper(amount); 363 | const [disputeID, _disputeTransactionId, disputeTransaction] = await createDisputeHelper( 364 | transactionId, 365 | transaction, 366 | ); 367 | 368 | await giveRulingHelper(disputeID, DisputeRuling.Sender); 369 | 370 | // Fully fund the loser side 371 | await fundAppealHelper( 372 | transactionId, 373 | disputeTransaction, 374 | receiver, 375 | loserAppealFee, 376 | TransactionParty.Receiver, 377 | ); 378 | const [_txPromise, _tx, receipt] = await fundAppealHelper( 379 | transactionId, 380 | disputeTransaction, 381 | sender, 382 | winnerAppealFee, 383 | TransactionParty.Sender, 384 | ); 385 | 386 | console.log(""); 387 | console.log("\tGas used by fundAppeal(): " + parseInt(receipt.gasUsed)); 388 | }); 389 | 390 | it("Estimate gas cost when withdrawing one round (winner side).", async () => { 391 | const loserAppealFee = 392 | arbitrationFee + (arbitrationFee * loserMultiplier) / MULTIPLIER_DIVISOR; 393 | const winnerAppealFee = 394 | arbitrationFee + (arbitrationFee * winnerMultiplier) / MULTIPLIER_DIVISOR; 395 | 396 | const [_receipt, transactionId, transaction] = await createTransactionHelper(amount); 397 | const [disputeID, _disputeTransactionId, disputeTransaction] = await createDisputeHelper( 398 | transactionId, 399 | transaction, 400 | ); 401 | 402 | await giveRulingHelper(disputeID, DisputeRuling.Sender); 403 | 404 | // Fully fund the loser side 405 | await fundAppealHelper( 406 | transactionId, 407 | disputeTransaction, 408 | receiver, 409 | loserAppealFee, 410 | TransactionParty.Receiver, 411 | ); 412 | await fundAppealHelper( 413 | transactionId, 414 | disputeTransaction, 415 | sender, 416 | winnerAppealFee, 417 | TransactionParty.Sender, 418 | ); 419 | 420 | // Give and execute final ruling 421 | const appealDisputeID = await arbitrator.getAppealDisputeID(disputeID); 422 | await giveFinalRulingHelper(appealDisputeID, DisputeRuling.Sender, disputeID); 423 | const [_ruleTransactionId, ruleTransaction] = await executeRulingHelper( 424 | transactionId, 425 | disputeTransaction, 426 | other, 427 | ); 428 | 429 | const [_txPromise, _tx, receipt] = await withdrawHelper( 430 | await sender.getAddress(), 431 | transactionId, 432 | ruleTransaction, 433 | 0, 434 | sender, 435 | ); 436 | 437 | console.log(""); 438 | console.log("\tGas used by withdrawFeesAndRewards(): " + parseInt(receipt.gasUsed)); 439 | }); 440 | 441 | it("Estimate gas cost when batch-withdrawing 5 rounds (winner side).", async () => { 442 | const loserAppealFee = 443 | arbitrationFee + (arbitrationFee * loserMultiplier) / MULTIPLIER_DIVISOR; 444 | const winnerAppealFee = 445 | arbitrationFee + (arbitrationFee * winnerMultiplier) / MULTIPLIER_DIVISOR; 446 | const roundsLength = 5; 447 | 448 | const [_receipt, transactionId, transaction] = await createTransactionHelper(amount); 449 | const [disputeID, _disputeTransactionId, disputeTransaction] = await createDisputeHelper( 450 | transactionId, 451 | transaction, 452 | ); 453 | 454 | let roundDisputeID; 455 | roundDisputeID = disputeID; 456 | for (var roundI = 0; roundI < roundsLength; roundI += 1) { 457 | await giveRulingHelper(roundDisputeID, DisputeRuling.Sender); 458 | // Fully fund both sides 459 | await fundAppealHelper( 460 | transactionId, 461 | disputeTransaction, 462 | crowdfunder1, 463 | loserAppealFee, 464 | TransactionParty.Receiver, 465 | ); 466 | await fundAppealHelper( 467 | transactionId, 468 | disputeTransaction, 469 | crowdfunder2, 470 | winnerAppealFee, 471 | TransactionParty.Sender, 472 | ); 473 | roundDisputeID = await arbitrator.getAppealDisputeID(disputeID); 474 | } 475 | 476 | // Give and execute final ruling 477 | await giveFinalRulingHelper(roundDisputeID, DisputeRuling.Sender, disputeID); 478 | const [_ruleTransactionId, ruleTransaction] = await executeRulingHelper( 479 | transactionId, 480 | disputeTransaction, 481 | other, 482 | ); 483 | 484 | // Batch-withdraw (checking if _cursor and _count arguments are working as expected). 485 | const txPromise = contract 486 | .connect(other) 487 | .batchRoundWithdraw(await crowdfunder2.getAddress(), transactionId, ruleTransaction, 0, 0); 488 | const tx = await txPromise; 489 | const receipt = await tx.wait(); 490 | 491 | console.log(""); 492 | console.log("\tGas used by batchRoundWithdraw(): " + parseInt(receipt.gasUsed)); 493 | }); 494 | }); 495 | 496 | /** 497 | * Creates a transaction by sender to receiver. 498 | * @param {number} _amount Amount in wei. 499 | * @returns {Array} Tx data. 500 | */ 501 | async function createTransactionHelper(_amount) { 502 | const receiverAddress = await receiver.getAddress(); 503 | const metaEvidence = metaEvidenceUri; 504 | 505 | const tx = await contract 506 | .connect(sender) 507 | .createTransaction(timeoutPayment, receiverAddress, metaEvidence, { 508 | value: _amount, 509 | }); 510 | const receipt = await tx.wait(); 511 | const [transactionId, transaction] = getEmittedEvent("TransactionStateUpdated", receipt).args; 512 | 513 | return [receipt, transactionId, transaction]; 514 | } 515 | 516 | /** 517 | * Make both sides pay arbitration fees. The transaction should have been previosuly created. 518 | * @param {number} _transactionId Id of the transaction. 519 | * @param {object} _transaction Current transaction object. 520 | * @param {number} fee Appeal round from which to withdraw the rewards. 521 | * @returns {Array} Tx data. 522 | */ 523 | async function createDisputeHelper(_transactionId, _transaction, fee = arbitrationFee) { 524 | // Pay fees, create dispute and validate events. 525 | const receiverSettlementTx = await contract 526 | .connect(receiver) 527 | .proposeSettlement(_transactionId, _transaction, _transaction.amount); 528 | const receiverSettltementReceipt = await receiverSettlementTx.wait(); 529 | 530 | await increaseTime(100); 531 | 532 | const [settlementTransactionId, settlementTransaction] = getEmittedEvent( 533 | "TransactionStateUpdated", 534 | receiverSettltementReceipt, 535 | ).args; 536 | 537 | const receiverTxPromise = contract 538 | .connect(receiver) 539 | .payArbitrationFeeByReceiver(settlementTransactionId, settlementTransaction, { 540 | value: fee, 541 | }); 542 | const receiverFeeTx = await receiverTxPromise; 543 | const receiverFeeReceipt = await receiverFeeTx.wait(); 544 | expect(receiverTxPromise) 545 | .to.emit(contract, "HasToPayFee") 546 | .withArgs(_transactionId, TransactionParty.Sender); 547 | const [receiverFeeTransactionId, receiverFeeTransaction] = getEmittedEvent( 548 | "TransactionStateUpdated", 549 | receiverFeeReceipt, 550 | ).args; 551 | const txPromise = contract 552 | .connect(sender) 553 | .payArbitrationFeeBySender(receiverFeeTransactionId, receiverFeeTransaction, { 554 | value: fee, 555 | }); 556 | const senderFeeTx = await txPromise; 557 | const senderFeeReceipt = await senderFeeTx.wait(); 558 | const [senderFeeTransactionId, senderFeeTransaction] = getEmittedEvent( 559 | "TransactionStateUpdated", 560 | senderFeeReceipt, 561 | ).args; 562 | expect(txPromise) 563 | .to.emit(contract, "Dispute") 564 | .withArgs( 565 | arbitrator.address, 566 | senderFeeTransaction.disputeID, 567 | senderFeeTransactionId, 568 | senderFeeTransactionId, 569 | ); 570 | expect(senderFeeTransaction.status).to.equal( 571 | TransactionStatus.DisputeCreated, 572 | "Invalid transaction status", 573 | ); 574 | return [senderFeeTransaction.disputeID, senderFeeTransactionId, senderFeeTransaction]; 575 | } 576 | 577 | /** 578 | * Give ruling (not final). 579 | * @param {number} disputeID dispute ID. 580 | * @param {number} ruling Ruling: None, Sender or Receiver. 581 | * @returns {Array} Tx data. 582 | */ 583 | async function giveRulingHelper(disputeID, ruling) { 584 | // Notice that rule() function is not called by the arbitrator, because the dispute is appealable. 585 | const txPromise = arbitrator.giveRuling(disputeID, ruling); 586 | const tx = await txPromise; 587 | const receipt = await tx.wait(); 588 | 589 | return [txPromise, tx, receipt]; 590 | } 591 | 592 | /** 593 | * Give final ruling and enforce it. 594 | * @param {number} disputeID dispute ID. 595 | * @param {number} ruling Ruling: None, Sender or Receiver. 596 | * @param {number} transactionDisputeId Initial dispute ID. 597 | * @returns {Array} Random integer in the range (0, max]. 598 | */ 599 | async function giveFinalRulingHelper(disputeID, ruling, transactionDisputeId = disputeID) { 600 | const firstTx = await arbitrator.giveRuling(disputeID, ruling); 601 | await firstTx.wait(); 602 | 603 | await increaseTime(appealTimeout + 1); 604 | 605 | const txPromise = arbitrator.giveRuling(disputeID, ruling); 606 | const tx = await txPromise; 607 | const receipt = await tx.wait(); 608 | 609 | expect(txPromise) 610 | .to.emit(contract, "Ruling") 611 | .withArgs(arbitrator.address, transactionDisputeId, ruling); 612 | 613 | return [txPromise, tx, receipt]; 614 | } 615 | 616 | /** 617 | * Execute the final ruling. 618 | * @param {number} transactionId Id of the transaction. 619 | * @param {object} transaction Current transaction object. 620 | * @param {address} caller Can be anyone. 621 | * @returns {Array} Transaction ID and the updated object. 622 | */ 623 | async function executeRulingHelper(transactionId, transaction, caller) { 624 | const tx = await contract.connect(caller).executeRuling(transactionId, transaction); 625 | const receipt = await tx.wait(); 626 | const [newTransactionId, newTransaction] = getEmittedEvent( 627 | "TransactionStateUpdated", 628 | receipt, 629 | ).args; 630 | 631 | return [newTransactionId, newTransaction]; 632 | } 633 | 634 | /** 635 | * Fund new appeal round. 636 | * @param {number} transactionId Id of the transaction. 637 | * @param {object} transaction Current transaction object. 638 | * @param {address} caller Can be anyone. 639 | * @param {number} contribution Contribution amount in wei. 640 | * @param {number} side Side to contribute to: Sender or Receiver. 641 | * @returns {Array} Tx data. 642 | */ 643 | async function fundAppealHelper(transactionId, transaction, caller, contribution, side) { 644 | const txPromise = contract 645 | .connect(caller) 646 | .fundAppeal(transactionId, transaction, side, { value: contribution }); 647 | const tx = await txPromise; 648 | const receipt = await tx.wait(); 649 | 650 | return [txPromise, tx, receipt]; 651 | } 652 | 653 | /** 654 | * Withdraw rewards to beneficiary. 655 | * @param {address} beneficiary Address of the round contributor. 656 | * @param {number} transactionId Id of the transaction. 657 | * @param {object} transaction Current transaction object. 658 | * @param {number} round Appeal round from which to withdraw the rewards. 659 | * @param {address} caller Can be anyone. 660 | * @returns {Array} Tx data. 661 | */ 662 | async function withdrawHelper(beneficiary, transactionId, transaction, round, caller) { 663 | const txPromise = contract 664 | .connect(caller) 665 | .withdrawFeesAndRewards(beneficiary, transactionId, transaction, round); 666 | const tx = await txPromise; 667 | const receipt = await tx.wait(); 668 | 669 | return [txPromise, tx, receipt]; 670 | } 671 | }); 672 | --------------------------------------------------------------------------------