From b89c748d24ab3eafebcd916fe16564ed49fe027b Mon Sep 17 00:00:00 2001 From: Theo Butler Date: Wed, 28 Jun 2023 15:58:30 -0400 Subject: [PATCH 01/23] Add initial Collateralization contract --- .github/workflows/test.yml | 34 ++++ .gitignore | 14 ++ .gitmodules | 6 + README.md | 4 + foundry.toml | 11 ++ lib/forge-std | 1 + lib/openzeppelin-contracts | 1 + models/collateralization.als | 105 +++++++++++++ models/theme.thm | 51 ++++++ remappings.txt | 5 + src/Collateralization.sol | 102 ++++++++++++ test/Collateralization.t.sol | 296 +++++++++++++++++++++++++++++++++++ 12 files changed, 630 insertions(+) create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 foundry.toml create mode 160000 lib/forge-std create mode 160000 lib/openzeppelin-contracts create mode 100644 models/collateralization.als create mode 100644 models/theme.thm create mode 100644 remappings.txt create mode 100644 src/Collateralization.sol create mode 100644 test/Collateralization.t.sol diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..09880b1 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,34 @@ +name: test + +on: workflow_dispatch + +env: + FOUNDRY_PROFILE: ci + +jobs: + check: + strategy: + fail-fast: true + + name: Foundry project + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Run Forge build + run: | + forge --version + forge build --sizes + id: build + + - name: Run Forge tests + run: | + forge test -vvv + id: test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..85198aa --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# Compiler files +cache/ +out/ + +# Ignores development broadcast logs +!/broadcast +/broadcast/*/31337/ +/broadcast/**/dry-run/ + +# Docs +docs/ + +# Dotenv file +.env diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..690924b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "lib/forge-std"] + path = lib/forge-std + url = https://github.com/foundry-rs/forge-std +[submodule "lib/openzeppelin-contracts"] + path = lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/README.md b/README.md index 0e8626c..f0e5749 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,6 @@ # horizon Contracts & Specs for Graph Horizon + +## Setup + +- Install [Foundry](https://book.getfoundry.sh/getting-started/installation) diff --git a/foundry.toml b/foundry.toml new file mode 100644 index 0000000..11c67c0 --- /dev/null +++ b/foundry.toml @@ -0,0 +1,11 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] + +# See more config options https://github.com/foundry-rs/foundry/tree/master/config + +[invariant] +fail_on_revert = false +depth = 10 +runs = 1000 diff --git a/lib/forge-std b/lib/forge-std new file mode 160000 index 0000000..e8a047e --- /dev/null +++ b/lib/forge-std @@ -0,0 +1 @@ +Subproject commit e8a047e3f40f13fa37af6fe14e6e06283d9a060e diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts new file mode 160000 index 0000000..e50c24f --- /dev/null +++ b/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit e50c24f5839db17f46991478384bfda14acfb830 diff --git a/models/collateralization.als b/models/collateralization.als new file mode 100644 index 0000000..c249619 --- /dev/null +++ b/models/collateralization.als @@ -0,0 +1,105 @@ +open util/boolean + +sig Addr { + var balance: Int, +} + +sig Deposit { + value: Int, + expiration: Int, + arbiter: Addr, + depositor: lone Addr, +} + +one sig Collateralization in Addr { + var deposits: set Deposit, + var epoch: Int, +} + +fact { + all d: Deposit { + d.value > 0 + d.expiration >= 0 + d.depositor != Collateralization + } + Collateralization.epoch >= 0 + let activeDeposits = {d: Collateralization.deposits | some d.depositor} | + Collateralization.balance >= (sum d: activeDeposits | d.value) +} + +fact { + always all a: Addr | a.balance >= 0 + always Collateralization.epoch' >= Collateralization.epoch + all d: Deposit | eventually d in Collateralization.deposits +} + +pred transfer[from, to: Addr, n: Int] { + n > 0 + from.balance >= n + from.balance' = sub[from.balance, n] + to.balance' = add[to.balance, n] +} + +pred prepareDeposit[sender: Addr, fund: Bool] { + some d: Deposit { + d not in Collateralization.deposits + Collateralization.epoch < d.expiration + Collateralization.deposits' = Collateralization.deposits + d + isTrue[fund] => { + d.depositor = sender + transfer[sender, Collateralization, d.value] + } else { + no d.depositor + Collateralization.balance' = Collateralization.balance + } + } +} + +pred fund[sender: Addr, d: Deposit] { + d in Collateralization.deposits + Collateralization.epoch < d.expiration + no d.depositor + d.depositor' = sender + transfer[sender, Collateralization, d.value] +} + +pred withdraw[sender: Addr, d: Deposit] { + d in Collateralization.deposits + Collateralization.epoch >= d.expiration + Collateralization.deposits' = Collateralization.deposits - d + transfer[Collateralization, d.depositor, d.value] +} + +pred slash[a: Addr, d: Deposit] { + d in Collateralization.deposits + some d.depositor + d.arbiter = a + d.expiration < Collateralization.epoch + Collateralization.deposits' = Collateralization.deposits - d + Collateralization.balance' = sub[Collateralization.balance, d.value] +} + +run {} for 3 but 3 Int + +pred skip { + Collateralization.balance' = Collateralization.balance + Collateralization.deposits' = Collateralization.deposits +} + +fact transitions { + always ( + (some sender: Addr, fund: Bool | prepareDeposit[sender, fund]) or + (some sender: Addr, d: Deposit | fund[sender, d]) or + (some sender: Addr, d: Deposit | withdraw[sender, d]) or + (some sender: Addr, d: Deposit | slash[sender, d]) or + skip + ) +} + +check { + always (let activeDeposits = {d: Collateralization.deposits | some d.depositor} | + Collateralization.balance >= (sum d: activeDeposits | d.value)) + // deposit can only be removed after expiration + always all d: Collateralization.deposits | + (Collateralization.epoch >= d.expiration) releases (d in Collateralization.deposits) +} for 3 but 5 steps, 3 Int diff --git a/models/theme.thm b/models/theme.thm new file mode 100644 index 0000000..c73317d --- /dev/null +++ b/models/theme.thm @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/remappings.txt b/remappings.txt new file mode 100644 index 0000000..3f2cd54 --- /dev/null +++ b/remappings.txt @@ -0,0 +1,5 @@ +ds-test/=lib/forge-std/lib/ds-test/src/ +erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/ +forge-std/=lib/forge-std/src/ +openzeppelin-contracts/=lib/openzeppelin-contracts/ +openzeppelin/=lib/openzeppelin-contracts/contracts/ diff --git a/src/Collateralization.sol b/src/Collateralization.sol new file mode 100644 index 0000000..8b0138a --- /dev/null +++ b/src/Collateralization.sol @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.sol"; + +/// @notice A `Deposit` describes a slashable, time-locked deposit of `value` tokens. A `Deposit` +/// might not be funded upon creation, in which case the `depositor` is set to the zero address. +/// Once a `Deposit` is funded (has a nonzero `depositor`), it will remain in the funded state until +/// the deposit is removed via withdrawal or slashing. A `Deposit` may only be withdrawn when +/// `block.timestamp >= expiration`. Withdrawal returns `value` tokens to the depositor. The +/// `arbiter` has the authority to slash the `Deposit` before `expiration`, which also burns `value` +/// tokens. +struct Deposit { + uint256 value; + uint128 expiration; + address arbiter; + address depositor; +} + +/// @notice This contract manages `Deposits` as described above. +contract Collateralization { + /// @notice Burnable ERC-20 token held by this contract. + ERC20Burnable public token; + /// @notice Mapping of deposit IDs to deposits. + mapping(uint128 => Deposit) public deposits; + /// @notice Counter for assigning new deposit IDs. + uint128 public lastID; + + /// @param _token the burnable ERC-20 token held by this contract. + constructor(ERC20Burnable _token) { + token = _token; + } + + /// @notice Create a new deposit, returning its associated ID. + /// @param value Token value of the new deposit. + /// @param expiration Expiration timestamp of the new deposit, in seconds. + /// @param arbiter Arbiter of the new deposit. + /// @param _fund True if the sender intends to fund the new deposit, false otherwise. + /// @return id Unique ID associated with the new deposit. + function prepare(uint256 value, uint128 expiration, address arbiter, bool _fund) public returns (uint128) { + require(value > 0, "value is zero"); + require(block.timestamp < expiration, "deposit expired"); + lastID += 1; + address depositor = _fund ? msg.sender : address(0); + deposits[lastID] = Deposit({value: value, expiration: expiration, arbiter: arbiter, depositor: depositor}); + if (_fund) { + bool transferSuccess = token.transferFrom(msg.sender, address(this), value); + require(transferSuccess, "transfer failed"); + } + return lastID; + } + + /// @notice Fund an existing deposit. The sender will be set as the depositor, granting them the + /// authority to withdraw upon expiration. + /// @param id ID of the associated deposit. + function fund(uint128 id) public { + Deposit memory deposit = deposits[id]; + require(block.timestamp < deposit.expiration, "deposit expired"); + require(deposit.depositor == address(0), "deposit funded"); + deposits[id].depositor = msg.sender; + bool transferSuccess = token.transferFrom(msg.sender, address(this), deposit.value); + require(transferSuccess, "transfer failed"); + } + + /// @notice Remove an expired deposit and return its associated tokens to the depositor. + /// @param id ID of the associated deposit. + function withdraw(uint128 id) public { + Deposit memory deposit = deposits[id]; + require(block.timestamp >= deposit.expiration, "deposit not expired"); + require(deposit.depositor != address(0), "depositor is zero"); + delete deposits[id]; + bool _transferSuccess = token.transfer(deposit.depositor, deposit.value); + require(_transferSuccess, "transfer failed"); + } + + /// @notice Remove a deposit prior to expiration and burn its associated tokens. This action can + /// only be performed by the arbiter of the deposit associated with the given ID. + /// @param id ID of the associated deposit. + function slash(uint128 id) public { + Deposit memory deposit = deposits[id]; + require(msg.sender == deposit.arbiter, "sender not arbiter"); + require(deposit.depositor != address(0), "deposit not funded"); + require(block.timestamp < deposit.expiration, "deposit expired"); + delete deposits[id]; + token.burn(deposit.value); + } + + /// @notice Returns the deposit associated with the given ID. + /// @param id ID of the associated deposit. + function getDeposit(uint128 id) public view returns (Deposit memory) { + Deposit memory deposit = deposits[id]; + require(deposit.value > 0, "deposit not found"); + return deposit; + } + + /// @notice Returns true if the deposit associated with the given ID is slashable, false + /// otherwise. A slashable deposit is one that is funded and has not expired. + /// @param id ID of the associated deposit. + function isSlashable(uint128 id) public view returns (bool) { + return (deposits[id].depositor != address(0)) && (block.timestamp < deposits[id].expiration); + } +} diff --git a/test/Collateralization.t.sol b/test/Collateralization.t.sol new file mode 100644 index 0000000..635d85f --- /dev/null +++ b/test/Collateralization.t.sol @@ -0,0 +1,296 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {CommonBase} from "forge-std/Base.sol"; +import {StdUtils} from "forge-std/StdUtils.sol"; +import {Test} from "forge-std/Test.sol"; +import {ERC20Burnable} from "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.sol"; +import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; +import {Collateralization, Deposit} from "../src/Collateralization.sol"; + +contract TestToken is ERC20Burnable { + constructor(uint256 _initialSupply) ERC20("MockCoin", "MOCK") { + _mint(msg.sender, _initialSupply); + } +} + +contract CollateralizationHandler is CommonBase, StdUtils { + Collateralization public collateralization; + uint256 tokenSupply; + uint128[] depositIDs; + address[] actors; + + constructor() { + tokenSupply = 900; + collateralization = new Collateralization(new TestToken(tokenSupply)); + actors = [address(1), address(2), address(3)]; + + uint256 index = 0; + while (index < actors.length) { + collateralization.token().transfer(actors[index], tokenSupply / (actors.length)); + index += 1; + } + } + + function warp(uint256 blocks) public { + vm.warp(block.timestamp + bound(blocks, 1, 10)); + } + + function prepare(uint256 value, uint256 expiration, uint256 sender, uint256 arbiter, bool _fund) + public + returns (uint128) + { + uint256 _value = bound(value, 0, tokenSupply / 10); + vm.startPrank(_genActor(sender)); + collateralization.token().approve(address(collateralization), _value); + uint128 id = collateralization.prepare(_value, _genExpiration(expiration), _genActor(arbiter), _fund); + vm.stopPrank(); + depositIDs.push(id); + return id; + } + + function fund(uint256 sender, uint256 id) public { + vm.prank(_genActor(sender)); + collateralization.fund(_genID(id)); + } + + function withdraw(uint256 sender, uint256 id) public { + uint128 id_ = _genID(id); + vm.prank(_genActor(sender)); + collateralization.withdraw(id_); + _removeDepositID(id_); + } + + function slash(uint256 sender, uint256 id) public { + uint128 _id = _genID(id); + vm.prank(_genActor(sender)); + collateralization.slash(_id); + _removeDepositID(_id); + } + + function fundedDepositTotal() public view returns (uint256) { + uint256 total = 0; + uint64 index = 0; + while (index < depositIDs.length) { + uint128 id = depositIDs[index]; + Deposit memory deposit = collateralization.getDeposit(id); + if (deposit.depositor != address(0)) { + total += deposit.value; + } + index += 1; + } + return total; + } + + function _removeDepositID(uint128 id) internal { + uint256 index = 0; + while (index < depositIDs.length) { + if (depositIDs[index] == id) { + depositIDs[index] = depositIDs[depositIDs.length - 1]; + depositIDs.pop(); + return; + } + index += 1; + } + } + + function _genID(uint256 seed) internal view returns (uint128) { + uint256 index = bound(seed, 0, depositIDs.length); + if (index == depositIDs.length) { + return type(uint128).max; + } + return depositIDs[index]; + } + + function _genActor(uint256 seed) internal view returns (address) { + return actors[bound(seed, 0, actors.length - 1)]; + } + + function _genExpiration(uint256 seed) internal view returns (uint128) { + return uint128(bound(seed, 0, 20)); + } +} + +contract CollateralizationInvariants is Test { + CollateralizationHandler public handler; + ERC20Burnable public token; + + function setUp() public { + handler = new CollateralizationHandler(); + token = handler.collateralization().token(); + targetContract(address(handler)); + } + + function invariant_depositBalance() public { + assertEq(token.balanceOf(address(handler.collateralization())), handler.fundedDepositTotal()); + } +} + +contract CollateralizationUnitTests is Test { + TestToken public token; + Collateralization public collateralization; + + function setUp() public { + token = new TestToken(1_000); + collateralization = new Collateralization(token); + } + + function test_Prepare() public { + uint128 expiration = uint128(block.timestamp) + 1; + uint256 initialBalance = token.balanceOf(address(this)); + + uint128 id1 = collateralization.prepare(1, expiration, address(0), false); + assertEq(token.balanceOf(address(this)), initialBalance); + assertEq(token.balanceOf(address(collateralization)), 0); + assertEq(collateralization.getDeposit(id1).depositor, address(0)); + assert(!collateralization.isSlashable(id1)); + + token.approve(address(collateralization), 1); + uint128 id2 = collateralization.prepare(1, expiration, address(0), true); + assertEq(token.balanceOf(address(this)), initialBalance - 1); + assertEq(token.balanceOf(address(collateralization)), 1); + assertEq(collateralization.getDeposit(id2).depositor, address(this)); + assert(collateralization.isSlashable(id2)); + + assertNotEq(id1, id2); + } + + function testFail_PrepareExpirationAtBlock() public { + token.approve(address(collateralization), 10); + uint128 expiration = uint128(block.timestamp); + collateralization.prepare(10, expiration, address(0), false); + } + + function testFail_PrepareExpirationBeforeBlock() public { + token.approve(address(collateralization), 10); + uint128 expiration = uint128(block.timestamp) - 1; + collateralization.prepare(10, expiration, address(0), false); + } + + function test_Fund() public { + uint128 expiration = uint128(block.timestamp) + 1; + uint256 initialBalance = token.balanceOf(address(this)); + uint128 id = collateralization.prepare(1, expiration, address(0), false); + token.approve(address(collateralization), 1); + collateralization.fund(id); + assertEq(token.balanceOf(address(this)), initialBalance - 1); + assertEq(token.balanceOf(address(collateralization)), 1); + assertEq(collateralization.getDeposit(id).depositor, address(this)); + assert(collateralization.isSlashable(id)); + } + + function testFail_FundAtExpiration() public { + uint128 expiration = uint128(block.timestamp) + 1; + uint128 id = collateralization.prepare(1, expiration, address(0), false); + token.approve(address(collateralization), 1); + vm.warp(expiration); + collateralization.fund(id); + } + + function testFail_FundAfterExpiration() public { + uint128 expiration = uint128(block.timestamp) + 1; + uint128 id = collateralization.prepare(1, expiration, address(0), false); + token.approve(address(collateralization), 1); + vm.warp(expiration + 1); + collateralization.fund(id); + } + + function testFail_FundNoPrepare() public { + token.approve(address(collateralization), 1); + collateralization.fund(1); + } + + function test_WithdrawAtExpiration() public { + uint128 expiration = uint128(block.timestamp) + 1; + uint256 initialBalance = token.balanceOf(address(this)); + token.approve(address(collateralization), 1); + uint128 id = collateralization.prepare(1, expiration, address(0), true); + vm.warp(expiration); + collateralization.withdraw(id); + assertEq(token.balanceOf(address(this)), initialBalance); + assertEq(token.balanceOf(address(collateralization)), 0); + assertEq(collateralization.isSlashable(id), false); + } + + function test_WithdrawAfterExpiration() public { + uint128 expiration = uint128(block.timestamp) + 1; + uint256 initialBalance = token.balanceOf(address(this)); + token.approve(address(collateralization), 1); + uint128 id = collateralization.prepare(1, expiration, address(0), true); + vm.warp(expiration + 1); + collateralization.withdraw(id); + assertEq(token.balanceOf(address(this)), initialBalance); + assertEq(token.balanceOf(address(collateralization)), 0); + } + + function testFail_WithdrawBeforeExpiration() public { + token.approve(address(collateralization), 1); + uint128 expiration = uint128(block.timestamp) + 3; + uint128 id = collateralization.prepare(1, expiration, address(0), true); + vm.warp(expiration - 1); + collateralization.withdraw(id); + } + + function testFail_WithdrawNoPrepare() public { + collateralization.withdraw(0); + } + + function test_WithdrawFromNonDepositor() public { + uint128 expiration = uint128(block.timestamp) + 1; + uint256 initialBalance = token.balanceOf(address(this)); + address other = 0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF; + vm.prank(other); + uint128 id = collateralization.prepare(1, expiration, address(0), false); + token.approve(address(collateralization), 1); + collateralization.fund(id); + assertEq(token.balanceOf(address(this)), initialBalance - 1); + vm.warp(expiration); + vm.prank(other); + collateralization.withdraw(id); + assertEq(token.balanceOf(address(this)), initialBalance); + } + + function test_Slash() public { + uint128 expiration = uint128(block.timestamp) + 3; + address arbiter = 0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF; + uint256 initialSupply = token.totalSupply(); + token.approve(address(collateralization), 1); + uint128 id = collateralization.prepare(1, expiration, arbiter, true); + vm.warp(expiration - 1); + vm.prank(arbiter); + collateralization.slash(id); + assertEq(collateralization.isSlashable(id), false); + assertEq(token.totalSupply(), initialSupply - 1); + } + + function testFail_SlashAtExpiration() public { + uint128 expiration = uint128(block.timestamp) + 3; + token.approve(address(collateralization), 1); + uint128 id = collateralization.prepare(1, expiration, address(this), true); + vm.warp(expiration); + collateralization.slash(id); + } + + function testFail_SlashAfterExpiration() public { + uint128 expiration = uint128(block.timestamp) + 3; + token.approve(address(collateralization), 1); + uint128 id = collateralization.prepare(1, expiration, address(this), true); + vm.warp(expiration + 1); + collateralization.slash(id); + } + + function testFail_SlashFromNonArbiter() public { + uint128 expiration = uint128(block.timestamp) + 3; + address arbiter = 0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF; + token.approve(address(collateralization), 1); + uint128 id = collateralization.prepare(1, expiration, arbiter, true); + vm.warp(expiration - 1); + collateralization.slash(id); + } + + function testFail_SlashNoFund() public { + uint128 expiration = uint128(block.timestamp) + 1; + uint128 id = collateralization.prepare(1, expiration, address(this), false); + collateralization.slash(id); + } +} From b5aac2d25fa4c38f981a72f8a37efa5e6c212ace Mon Sep 17 00:00:00 2001 From: Theo Butler Date: Thu, 13 Jul 2023 20:22:49 -0400 Subject: [PATCH 02/23] Rewrite collateralization --- models/collateralization.als | 105 ----------- models/theme.thm | 51 ------ src/Collateralization.sol | 190 +++++++++++++------- test/Collateralization.t.sol | 333 +++++++++++++++++++---------------- 4 files changed, 309 insertions(+), 370 deletions(-) delete mode 100644 models/collateralization.als delete mode 100644 models/theme.thm diff --git a/models/collateralization.als b/models/collateralization.als deleted file mode 100644 index c249619..0000000 --- a/models/collateralization.als +++ /dev/null @@ -1,105 +0,0 @@ -open util/boolean - -sig Addr { - var balance: Int, -} - -sig Deposit { - value: Int, - expiration: Int, - arbiter: Addr, - depositor: lone Addr, -} - -one sig Collateralization in Addr { - var deposits: set Deposit, - var epoch: Int, -} - -fact { - all d: Deposit { - d.value > 0 - d.expiration >= 0 - d.depositor != Collateralization - } - Collateralization.epoch >= 0 - let activeDeposits = {d: Collateralization.deposits | some d.depositor} | - Collateralization.balance >= (sum d: activeDeposits | d.value) -} - -fact { - always all a: Addr | a.balance >= 0 - always Collateralization.epoch' >= Collateralization.epoch - all d: Deposit | eventually d in Collateralization.deposits -} - -pred transfer[from, to: Addr, n: Int] { - n > 0 - from.balance >= n - from.balance' = sub[from.balance, n] - to.balance' = add[to.balance, n] -} - -pred prepareDeposit[sender: Addr, fund: Bool] { - some d: Deposit { - d not in Collateralization.deposits - Collateralization.epoch < d.expiration - Collateralization.deposits' = Collateralization.deposits + d - isTrue[fund] => { - d.depositor = sender - transfer[sender, Collateralization, d.value] - } else { - no d.depositor - Collateralization.balance' = Collateralization.balance - } - } -} - -pred fund[sender: Addr, d: Deposit] { - d in Collateralization.deposits - Collateralization.epoch < d.expiration - no d.depositor - d.depositor' = sender - transfer[sender, Collateralization, d.value] -} - -pred withdraw[sender: Addr, d: Deposit] { - d in Collateralization.deposits - Collateralization.epoch >= d.expiration - Collateralization.deposits' = Collateralization.deposits - d - transfer[Collateralization, d.depositor, d.value] -} - -pred slash[a: Addr, d: Deposit] { - d in Collateralization.deposits - some d.depositor - d.arbiter = a - d.expiration < Collateralization.epoch - Collateralization.deposits' = Collateralization.deposits - d - Collateralization.balance' = sub[Collateralization.balance, d.value] -} - -run {} for 3 but 3 Int - -pred skip { - Collateralization.balance' = Collateralization.balance - Collateralization.deposits' = Collateralization.deposits -} - -fact transitions { - always ( - (some sender: Addr, fund: Bool | prepareDeposit[sender, fund]) or - (some sender: Addr, d: Deposit | fund[sender, d]) or - (some sender: Addr, d: Deposit | withdraw[sender, d]) or - (some sender: Addr, d: Deposit | slash[sender, d]) or - skip - ) -} - -check { - always (let activeDeposits = {d: Collateralization.deposits | some d.depositor} | - Collateralization.balance >= (sum d: activeDeposits | d.value)) - // deposit can only be removed after expiration - always all d: Collateralization.deposits | - (Collateralization.epoch >= d.expiration) releases (d in Collateralization.deposits) -} for 3 but 5 steps, 3 Int diff --git a/models/theme.thm b/models/theme.thm deleted file mode 100644 index c73317d..0000000 --- a/models/theme.thm +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Collateralization.sol b/src/Collateralization.sol index 8b0138a..1b2c579 100644 --- a/src/Collateralization.sol +++ b/src/Collateralization.sol @@ -3,100 +3,156 @@ pragma solidity ^0.8.13; import "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.sol"; -/// @notice A `Deposit` describes a slashable, time-locked deposit of `value` tokens. A `Deposit` -/// might not be funded upon creation, in which case the `depositor` is set to the zero address. -/// Once a `Deposit` is funded (has a nonzero `depositor`), it will remain in the funded state until -/// the deposit is removed via withdrawal or slashing. A `Deposit` may only be withdrawn when -/// `block.timestamp >= expiration`. Withdrawal returns `value` tokens to the depositor. The -/// `arbiter` has the authority to slash the `Deposit` before `expiration`, which also burns `value` -/// tokens. +/// A `Deposit` describes a slashable, time-locked deposit of `value` tokens. A `Deposit` must be `locked` to provide +/// the following invariants: +/// - A `Deposit` may only be withdrawn when `block.timestamp >= expiration`. Withdrawal returns `value` tokens to the +/// depositor. +/// - The `arbiter` has the authority to slash the `Deposit` before `expiration`, which also burns `value` tokens. struct Deposit { + address depositor; + address arbiter; uint256 value; uint128 expiration; - address arbiter; - address depositor; + DepositState state; +} + +/// ┌────────┐ ┌──────┐ ┌─────────┐ ┌───────┐ +/// │Unlocked│ │Locked│ │Withdrawn│ │Slashed│ +/// └───┬────┘ └──┬───┘ └────┬────┘ └───┬───┘ +/// │ lock │ │ │ +/// │ ─────────────────> │ │ +/// │ │ │ │ +/// │ │ withdraw │ │ +/// │ │ ─────────────────>│ │ +/// │ │ │ │ +/// │ withdraw │ │ +/// │ ────────────────────────────────────>│ │ +/// │ │ │ │ +/// │ │ slash │ +/// │ │ ─────────────────────────────────────>│ +/// │ │ │ │ +/// │ deposit │ │ +/// │ <────────────────────────────────────│ │ +/// │ │ │ │ +/// │ │ deposit │ │ +/// │ <────────────────────────────────────────────────────────│ +/// ┌───┴────┐ ┌──┴───┐ ┌────┴────┐ ┌───┴───┐ +/// │Unlocked│ │Locked│ │Withdrawn│ │Slashed│ +/// └────────┘ └──────┘ └─────────┘ └───────┘ +enum DepositState { + Unlocked, // 0x00 TODO: opt + Locked, // 0x01 + Withdrawn, // 0x10 + Slashed // 0x11 } -/// @notice This contract manages `Deposits` as described above. +uint8 constant _CanDeposit = 0x00; + +/// Deposit in unexpected state. +error UnexpectedState(DepositState state); +/// Deposit value is zero. +error ZeroValue(); +/// Deposit expiration in unexpected state. +error Expired(bool expired); +/// Slash called by an address that isn't the deposit's arbiter. +error NotArbiter(); +/// Deposit does not exist. +error NotFound(); + +/// This contract manages `Deposit`s as described above. contract Collateralization { - /// @notice Burnable ERC-20 token held by this contract. + /// Burnable ERC-20 token held by this contract. ERC20Burnable public token; - /// @notice Mapping of deposit IDs to deposits. + /// Mapping of deposit IDs to deposits. mapping(uint128 => Deposit) public deposits; - /// @notice Counter for assigning new deposit IDs. + /// Counter for assigning new deposit IDs. uint128 public lastID; /// @param _token the burnable ERC-20 token held by this contract. constructor(ERC20Burnable _token) { + // TODO: use a constant token = _token; } - /// @notice Create a new deposit, returning its associated ID. - /// @param value Token value of the new deposit. - /// @param expiration Expiration timestamp of the new deposit, in seconds. - /// @param arbiter Arbiter of the new deposit. - /// @param _fund True if the sender intends to fund the new deposit, false otherwise. + /// Create a new deposit, returning its associated ID. + /// @param _id ID of the deposit ID to reuse. This should be set to zero to receive a new ID. IDs may only be reused + /// if the associated deposit is withdrawn or slashed. + /// @param _value Token value of the new deposit. + /// @param _expiration Expiration timestamp of the new deposit, in seconds. + /// @param _arbiter Arbiter of the new deposit. /// @return id Unique ID associated with the new deposit. - function prepare(uint256 value, uint128 expiration, address arbiter, bool _fund) public returns (uint128) { - require(value > 0, "value is zero"); - require(block.timestamp < expiration, "deposit expired"); - lastID += 1; - address depositor = _fund ? msg.sender : address(0); - deposits[lastID] = Deposit({value: value, expiration: expiration, arbiter: arbiter, depositor: depositor}); - if (_fund) { - bool transferSuccess = token.transferFrom(msg.sender, address(this), value); - require(transferSuccess, "transfer failed"); + function deposit(uint128 _id, uint256 _value, uint128 _expiration, address _arbiter) public returns (uint128) { + if (_value == 0) revert ZeroValue(); + if (block.timestamp >= _expiration) revert Expired(true); + if (_id == 0) { + lastID += 1; + _id = lastID; + } else { + DepositState _state = getDeposit(_id).state; + if ((_state != DepositState.Withdrawn) && (_state != DepositState.Slashed)) revert UnexpectedState(_state); } - return lastID; + deposits[_id] = Deposit({ + depositor: msg.sender, + arbiter: _arbiter, + value: _value, + expiration: _expiration, + state: DepositState.Unlocked + }); + bool _transferSuccess = token.transferFrom(msg.sender, address(this), _value); + require(_transferSuccess, "transfer failed"); + return _id; } - /// @notice Fund an existing deposit. The sender will be set as the depositor, granting them the - /// authority to withdraw upon expiration. - /// @param id ID of the associated deposit. - function fund(uint128 id) public { - Deposit memory deposit = deposits[id]; - require(block.timestamp < deposit.expiration, "deposit expired"); - require(deposit.depositor == address(0), "deposit funded"); - deposits[id].depositor = msg.sender; - bool transferSuccess = token.transferFrom(msg.sender, address(this), deposit.value); - require(transferSuccess, "transfer failed"); + /// Lock the deposit associated with the given ID. This makes the deposit slashable until the deposit + /// expiration. + /// @param _id ID of the associated deposit. + function lock(uint128 _id) public { + Deposit memory _deposit = getDeposit(_id); + if (_deposit.state != DepositState.Unlocked) revert UnexpectedState(_deposit.state); + if (block.timestamp >= _deposit.expiration) revert Expired(true); + deposits[_id].state = DepositState.Locked; } - /// @notice Remove an expired deposit and return its associated tokens to the depositor. - /// @param id ID of the associated deposit. - function withdraw(uint128 id) public { - Deposit memory deposit = deposits[id]; - require(block.timestamp >= deposit.expiration, "deposit not expired"); - require(deposit.depositor != address(0), "depositor is zero"); - delete deposits[id]; - bool _transferSuccess = token.transfer(deposit.depositor, deposit.value); + /// Unlock the deposit associated with the given ID and return its associated tokens to the depositor. + /// @param _id ID of the associated deposit. + function withdraw(uint128 _id) public { + Deposit memory _deposit = getDeposit(_id); + DepositState _state = deposits[_id].state; + if (_state == DepositState.Locked) { + if (block.timestamp < _deposit.expiration) revert Expired(false); + } else if (_state != DepositState.Unlocked) { + revert UnexpectedState(_state); + } + deposits[_id].state = DepositState.Withdrawn; + bool _transferSuccess = token.transfer(_deposit.depositor, _deposit.value); require(_transferSuccess, "transfer failed"); } - /// @notice Remove a deposit prior to expiration and burn its associated tokens. This action can - /// only be performed by the arbiter of the deposit associated with the given ID. - /// @param id ID of the associated deposit. - function slash(uint128 id) public { - Deposit memory deposit = deposits[id]; - require(msg.sender == deposit.arbiter, "sender not arbiter"); - require(deposit.depositor != address(0), "deposit not funded"); - require(block.timestamp < deposit.expiration, "deposit expired"); - delete deposits[id]; - token.burn(deposit.value); + /// Remove a deposit prior to expiration and burn its associated tokens. This action can only be performed by the + /// arbiter of the deposit associated with the given ID. + /// @param _id ID of the associated deposit. + function slash(uint128 _id) public { + Deposit memory _deposit = getDeposit(_id); + if (msg.sender != _deposit.arbiter) revert NotArbiter(); + if (_deposit.state != DepositState.Locked) revert UnexpectedState(_deposit.state); + if (block.timestamp >= _deposit.expiration) revert Expired(true); + deposits[_id].state = DepositState.Slashed; + token.burn(_deposit.value); } - /// @notice Returns the deposit associated with the given ID. - /// @param id ID of the associated deposit. - function getDeposit(uint128 id) public view returns (Deposit memory) { - Deposit memory deposit = deposits[id]; - require(deposit.value > 0, "deposit not found"); - return deposit; + /// Return the deposit associated with the given ID. + /// @param _id ID of the associated deposit. + function getDeposit(uint128 _id) public view returns (Deposit memory) { + Deposit memory _deposit = deposits[_id]; + if (_deposit.value == 0) revert NotFound(); + return _deposit; } - /// @notice Returns true if the deposit associated with the given ID is slashable, false - /// otherwise. A slashable deposit is one that is funded and has not expired. - /// @param id ID of the associated deposit. - function isSlashable(uint128 id) public view returns (bool) { - return (deposits[id].depositor != address(0)) && (block.timestamp < deposits[id].expiration); + /// Return true if the deposit associated with the given ID is slashable, false otherwise. A slashable deposit is + /// locked and not expired. + /// @param _id ID of the associated deposit. + function isSlashable(uint128 _id) public view returns (bool) { + Deposit memory _deposit = getDeposit(_id); + return (_deposit.state == DepositState.Locked) && (block.timestamp < _deposit.expiration); } } diff --git a/test/Collateralization.t.sol b/test/Collateralization.t.sol index 635d85f..ef798f8 100644 --- a/test/Collateralization.t.sol +++ b/test/Collateralization.t.sol @@ -6,7 +6,7 @@ import {StdUtils} from "forge-std/StdUtils.sol"; import {Test} from "forge-std/Test.sol"; import {ERC20Burnable} from "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.sol"; import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; -import {Collateralization, Deposit} from "../src/Collateralization.sol"; +import {Collateralization, Deposit, DepositState} from "../src/Collateralization.sol"; contract TestToken is ERC20Burnable { constructor(uint256 _initialSupply) ERC20("MockCoin", "MOCK") { @@ -25,10 +25,10 @@ contract CollateralizationHandler is CommonBase, StdUtils { collateralization = new Collateralization(new TestToken(tokenSupply)); actors = [address(1), address(2), address(3)]; - uint256 index = 0; - while (index < actors.length) { - collateralization.token().transfer(actors[index], tokenSupply / (actors.length)); - index += 1; + uint256 _index = 0; + while (_index < actors.length) { + collateralization.token().transfer(actors[_index], tokenSupply / (actors.length)); + _index += 1; } } @@ -36,78 +36,76 @@ contract CollateralizationHandler is CommonBase, StdUtils { vm.warp(block.timestamp + bound(blocks, 1, 10)); } - function prepare(uint256 value, uint256 expiration, uint256 sender, uint256 arbiter, bool _fund) + function deposit(uint256 __sender, uint256 __value, uint256 __expiration, uint256 __arbiter) public returns (uint128) { - uint256 _value = bound(value, 0, tokenSupply / 10); - vm.startPrank(_genActor(sender)); + address _depositor = _genActor(__sender); + uint256 _value = bound(__value, 1, collateralization.token().balanceOf(_depositor)); + vm.startPrank(_depositor); collateralization.token().approve(address(collateralization), _value); - uint128 id = collateralization.prepare(_value, _genExpiration(expiration), _genActor(arbiter), _fund); + uint128 _id = collateralization.deposit(0, _value, _genExpiration(__expiration), _genActor(__arbiter)); vm.stopPrank(); - depositIDs.push(id); - return id; + depositIDs.push(_id); + return _id; } - function fund(uint256 sender, uint256 id) public { - vm.prank(_genActor(sender)); - collateralization.fund(_genID(id)); + function lock(uint256 __sender, uint256 __id) public { + vm.prank(_genActor(__sender)); + collateralization.lock(_genID(__id)); } - function withdraw(uint256 sender, uint256 id) public { - uint128 id_ = _genID(id); - vm.prank(_genActor(sender)); - collateralization.withdraw(id_); - _removeDepositID(id_); + function withdraw(uint256 __sender, uint256 __id) public { + uint128 _id = _genID(__id); + vm.prank(_genActor(__sender)); + collateralization.withdraw(_id); + _removeDepositID(_id); } - function slash(uint256 sender, uint256 id) public { - uint128 _id = _genID(id); - vm.prank(_genActor(sender)); + function slash(uint256 __sender, uint256 __id) public { + uint128 _id = _genID(__id); + vm.prank(_genActor(__sender)); collateralization.slash(_id); + assert(collateralization.isSlashable(_id)); _removeDepositID(_id); } - function fundedDepositTotal() public view returns (uint256) { + function depositTotal() public view returns (uint256) { uint256 total = 0; - uint64 index = 0; - while (index < depositIDs.length) { - uint128 id = depositIDs[index]; - Deposit memory deposit = collateralization.getDeposit(id); - if (deposit.depositor != address(0)) { - total += deposit.value; + uint64 _index = 0; + while (_index < depositIDs.length) { + uint128 _id = depositIDs[_index]; + Deposit memory _deposit = collateralization.getDeposit(_id); + if (_deposit.depositor != address(0)) { + total += _deposit.value; } - index += 1; + _index += 1; } return total; } - function _removeDepositID(uint128 id) internal { - uint256 index = 0; - while (index < depositIDs.length) { - if (depositIDs[index] == id) { - depositIDs[index] = depositIDs[depositIDs.length - 1]; + function _removeDepositID(uint128 _id) internal { + uint256 _index = 0; + while (_index < depositIDs.length) { + if (depositIDs[_index] == _id) { + depositIDs[_index] = depositIDs[depositIDs.length - 1]; depositIDs.pop(); return; } - index += 1; + _index += 1; } } - function _genID(uint256 seed) internal view returns (uint128) { - uint256 index = bound(seed, 0, depositIDs.length); - if (index == depositIDs.length) { - return type(uint128).max; - } - return depositIDs[index]; + function _genID(uint256 _seed) internal view returns (uint128) { + return depositIDs[bound(_seed, 0, depositIDs.length - 1)]; } - function _genActor(uint256 seed) internal view returns (address) { - return actors[bound(seed, 0, actors.length - 1)]; + function _genActor(uint256 _seed) internal view returns (address) { + return actors[bound(_seed, 0, actors.length - 1)]; } - function _genExpiration(uint256 seed) internal view returns (uint128) { - return uint128(bound(seed, 0, 20)); + function _genExpiration(uint256 _seed) internal view returns (uint128) { + return uint128(bound(_seed, 1, 20)); } } @@ -122,7 +120,7 @@ contract CollateralizationInvariants is Test { } function invariant_depositBalance() public { - assertEq(token.balanceOf(address(handler.collateralization())), handler.fundedDepositTotal()); + assertEq(token.balanceOf(address(handler.collateralization())), handler.depositTotal()); } } @@ -135,162 +133,203 @@ contract CollateralizationUnitTests is Test { collateralization = new Collateralization(token); } - function test_Prepare() public { - uint128 expiration = uint128(block.timestamp) + 1; - uint256 initialBalance = token.balanceOf(address(this)); + function test_Deposit() public { + uint128 _expiration = uint128(block.timestamp) + 1; + uint256 _initialBalance = token.balanceOf(address(this)); + token.approve(address(collateralization), 1); + uint128 _id = collateralization.deposit(0, 1, _expiration, address(0)); + assertEq(token.balanceOf(address(this)), _initialBalance - 1); + assertEq(token.balanceOf(address(collateralization)), 1); + assertEq(collateralization.getDeposit(_id).depositor, address(this)); + assertEq(uint256(collateralization.getDeposit(_id).state), uint256(DepositState.Unlocked)); + assertEq(collateralization.isSlashable(_id), false); + } - uint128 id1 = collateralization.prepare(1, expiration, address(0), false); - assertEq(token.balanceOf(address(this)), initialBalance); - assertEq(token.balanceOf(address(collateralization)), 0); - assertEq(collateralization.getDeposit(id1).depositor, address(0)); - assert(!collateralization.isSlashable(id1)); + function test_DepositUniqueID() public { + uint128 _expiration = uint128(block.timestamp) + 1; + token.approve(address(collateralization), 2); + uint128 _id1 = collateralization.deposit(0, 1, _expiration, address(0)); + uint128 _id2 = collateralization.deposit(0, 1, _expiration, address(0)); + assertNotEq(_id1, _id2); + } + function testFail_DepositExpirationAtBlock() public { + uint128 _expiration = uint128(block.timestamp); token.approve(address(collateralization), 1); - uint128 id2 = collateralization.prepare(1, expiration, address(0), true); - assertEq(token.balanceOf(address(this)), initialBalance - 1); - assertEq(token.balanceOf(address(collateralization)), 1); - assertEq(collateralization.getDeposit(id2).depositor, address(this)); - assert(collateralization.isSlashable(id2)); + collateralization.deposit(0, 1, _expiration, address(0)); + } - assertNotEq(id1, id2); + function testFail_DepositExpirationBeforeBlock() public { + uint128 _expiration = uint128(block.timestamp) - 1; + token.approve(address(collateralization), 1); + collateralization.deposit(0, 1, _expiration, address(0)); } - function testFail_PrepareExpirationAtBlock() public { - token.approve(address(collateralization), 10); - uint128 expiration = uint128(block.timestamp); - collateralization.prepare(10, expiration, address(0), false); + function testFail_DepositReuseLocked() public { + uint128 _expiration = uint128(block.timestamp) + 1; + token.approve(address(collateralization), 1); + uint128 _id = collateralization.deposit(0, 1, _expiration, address(0)); + collateralization.lock(_id); + token.approve(address(collateralization), 1); + collateralization.deposit(_id, 1, _expiration, address(0)); } - function testFail_PrepareExpirationBeforeBlock() public { - token.approve(address(collateralization), 10); - uint128 expiration = uint128(block.timestamp) - 1; - collateralization.prepare(10, expiration, address(0), false); + function test_DepositReuseAfterWithdraw() public { + uint128 _expiration = uint128(block.timestamp) + 1; + token.approve(address(collateralization), 1); + uint128 _id1 = collateralization.deposit(0, 1, _expiration, address(0)); + collateralization.withdraw(_id1); + token.approve(address(collateralization), 1); + uint128 _id2 = collateralization.deposit(_id1, 1, _expiration, address(0)); + assertEq(_id1, _id2); } - function test_Fund() public { - uint128 expiration = uint128(block.timestamp) + 1; - uint256 initialBalance = token.balanceOf(address(this)); - uint128 id = collateralization.prepare(1, expiration, address(0), false); + function test_DepositReuseAfterSlash() public { + uint128 _expiration = uint128(block.timestamp) + 1; token.approve(address(collateralization), 1); - collateralization.fund(id); - assertEq(token.balanceOf(address(this)), initialBalance - 1); - assertEq(token.balanceOf(address(collateralization)), 1); - assertEq(collateralization.getDeposit(id).depositor, address(this)); - assert(collateralization.isSlashable(id)); + uint128 _id1 = collateralization.deposit(0, 1, _expiration, address(this)); + collateralization.lock(_id1); + collateralization.slash(_id1); + token.approve(address(collateralization), 1); + uint128 _id2 = collateralization.deposit(_id1, 1, _expiration, address(0)); + assertEq(_id1, _id2); } - function testFail_FundAtExpiration() public { - uint128 expiration = uint128(block.timestamp) + 1; - uint128 id = collateralization.prepare(1, expiration, address(0), false); + function test_Lock() public { + uint128 _expiration = uint128(block.timestamp) + 1; token.approve(address(collateralization), 1); - vm.warp(expiration); - collateralization.fund(id); + uint128 _id = collateralization.deposit(0, 1, _expiration, address(0)); + assertEq(uint256(collateralization.getDeposit(_id).state), uint256(DepositState.Unlocked)); + collateralization.lock(_id); + assertEq(uint256(collateralization.getDeposit(_id).state), uint256(DepositState.Locked)); + assertEq(collateralization.isSlashable(_id), true); } - function testFail_FundAfterExpiration() public { - uint128 expiration = uint128(block.timestamp) + 1; - uint128 id = collateralization.prepare(1, expiration, address(0), false); + function testFail_LockAtExpiration() public { + uint128 _expiration = uint128(block.timestamp) + 1; token.approve(address(collateralization), 1); - vm.warp(expiration + 1); - collateralization.fund(id); + uint128 _id = collateralization.deposit(0, 1, _expiration, address(0)); + vm.warp(_expiration); + collateralization.lock(_id); } - function testFail_FundNoPrepare() public { + function testFail_LockAfterExpiration() public { + uint128 _expiration = uint128(block.timestamp) + 1; token.approve(address(collateralization), 1); - collateralization.fund(1); + uint128 _id = collateralization.deposit(0, 1, _expiration, address(0)); + vm.warp(_expiration + 1); + collateralization.lock(_id); + } + + function testFail_getDepositNoDeposit() public view { + collateralization.getDeposit(0); } function test_WithdrawAtExpiration() public { - uint128 expiration = uint128(block.timestamp) + 1; - uint256 initialBalance = token.balanceOf(address(this)); + uint128 _expiration = uint128(block.timestamp) + 1; + uint256 _initialBalance = token.balanceOf(address(this)); token.approve(address(collateralization), 1); - uint128 id = collateralization.prepare(1, expiration, address(0), true); - vm.warp(expiration); - collateralization.withdraw(id); - assertEq(token.balanceOf(address(this)), initialBalance); + uint128 _id = collateralization.deposit(0, 1, _expiration, address(0)); + collateralization.lock(_id); + vm.warp(_expiration); + collateralization.withdraw(_id); + assertEq(token.balanceOf(address(this)), _initialBalance); assertEq(token.balanceOf(address(collateralization)), 0); - assertEq(collateralization.isSlashable(id), false); + assertEq(uint256(collateralization.getDeposit(_id).state), uint256(DepositState.Withdrawn)); } function test_WithdrawAfterExpiration() public { - uint128 expiration = uint128(block.timestamp) + 1; - uint256 initialBalance = token.balanceOf(address(this)); + uint128 _expiration = uint128(block.timestamp) + 1; + uint256 _initialBalance = token.balanceOf(address(this)); token.approve(address(collateralization), 1); - uint128 id = collateralization.prepare(1, expiration, address(0), true); - vm.warp(expiration + 1); - collateralization.withdraw(id); - assertEq(token.balanceOf(address(this)), initialBalance); + uint128 _id = collateralization.deposit(0, 1, _expiration, address(0)); + collateralization.lock(_id); + vm.warp(_expiration + 1); + collateralization.withdraw(_id); + assertEq(token.balanceOf(address(this)), _initialBalance); assertEq(token.balanceOf(address(collateralization)), 0); } function testFail_WithdrawBeforeExpiration() public { token.approve(address(collateralization), 1); - uint128 expiration = uint128(block.timestamp) + 3; - uint128 id = collateralization.prepare(1, expiration, address(0), true); - vm.warp(expiration - 1); - collateralization.withdraw(id); + uint128 _expiration = uint128(block.timestamp) + 3; + uint128 _id = collateralization.deposit(0, 1, _expiration, address(0)); + collateralization.lock(_id); + vm.warp(_expiration - 1); + collateralization.withdraw(_id); } - function testFail_WithdrawNoPrepare() public { - collateralization.withdraw(0); + function test_WithdrawLocked() public { + uint128 _expiration = uint128(block.timestamp) + 1; + uint256 _initialBalance = token.balanceOf(address(this)); + token.approve(address(collateralization), 1); + uint128 _id = collateralization.deposit(0, 1, _expiration, address(this)); + collateralization.lock(_id); + vm.warp(_expiration); + collateralization.withdraw(_id); + assertEq(token.balanceOf(address(this)), _initialBalance); + assertEq(token.balanceOf(address(collateralization)), 0); } function test_WithdrawFromNonDepositor() public { - uint128 expiration = uint128(block.timestamp) + 1; - uint256 initialBalance = token.balanceOf(address(this)); - address other = 0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF; - vm.prank(other); - uint128 id = collateralization.prepare(1, expiration, address(0), false); + uint128 _expiration = uint128(block.timestamp) + 1; + uint256 _initialBalance = token.balanceOf(address(this)); + address _other = 0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF; token.approve(address(collateralization), 1); - collateralization.fund(id); - assertEq(token.balanceOf(address(this)), initialBalance - 1); - vm.warp(expiration); - vm.prank(other); - collateralization.withdraw(id); - assertEq(token.balanceOf(address(this)), initialBalance); + uint128 _id = collateralization.deposit(0, 1, _expiration, address(0)); + collateralization.lock(_id); + vm.warp(_expiration); + vm.prank(_other); + collateralization.withdraw(_id); + assertEq(token.balanceOf(address(this)), _initialBalance); } function test_Slash() public { - uint128 expiration = uint128(block.timestamp) + 3; - address arbiter = 0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF; - uint256 initialSupply = token.totalSupply(); + uint128 _expiration = uint128(block.timestamp) + 3; + address _arbiter = 0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF; + uint256 _initialSupply = token.totalSupply(); token.approve(address(collateralization), 1); - uint128 id = collateralization.prepare(1, expiration, arbiter, true); - vm.warp(expiration - 1); - vm.prank(arbiter); - collateralization.slash(id); - assertEq(collateralization.isSlashable(id), false); - assertEq(token.totalSupply(), initialSupply - 1); + uint128 _id = collateralization.deposit(0, 1, _expiration, _arbiter); + collateralization.lock(_id); + vm.warp(_expiration - 1); + vm.prank(_arbiter); + collateralization.slash(_id); + assertEq(token.totalSupply(), _initialSupply - 1); + assertEq(uint256(collateralization.getDeposit(_id).state), uint256(DepositState.Slashed)); } function testFail_SlashAtExpiration() public { - uint128 expiration = uint128(block.timestamp) + 3; + uint128 _expiration = uint128(block.timestamp) + 3; token.approve(address(collateralization), 1); - uint128 id = collateralization.prepare(1, expiration, address(this), true); - vm.warp(expiration); - collateralization.slash(id); + uint128 _id = collateralization.deposit(0, 1, _expiration, address(this)); + collateralization.lock(_id); + vm.warp(_expiration); + collateralization.slash(_id); } function testFail_SlashAfterExpiration() public { - uint128 expiration = uint128(block.timestamp) + 3; + uint128 _expiration = uint128(block.timestamp) + 3; token.approve(address(collateralization), 1); - uint128 id = collateralization.prepare(1, expiration, address(this), true); - vm.warp(expiration + 1); - collateralization.slash(id); + uint128 _id = collateralization.deposit(0, 1, _expiration, address(this)); + collateralization.lock(_id); + vm.warp(_expiration + 1); + collateralization.slash(_id); } - function testFail_SlashFromNonArbiter() public { - uint128 expiration = uint128(block.timestamp) + 3; - address arbiter = 0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF; + function testFail_SlashUnlocked() public { + uint128 _expiration = uint128(block.timestamp) + 3; token.approve(address(collateralization), 1); - uint128 id = collateralization.prepare(1, expiration, arbiter, true); - vm.warp(expiration - 1); - collateralization.slash(id); + uint128 _id = collateralization.deposit(0, 1, _expiration, address(this)); + vm.warp(_expiration + 1); + collateralization.slash(_id); } - function testFail_SlashNoFund() public { - uint128 expiration = uint128(block.timestamp) + 1; - uint128 id = collateralization.prepare(1, expiration, address(this), false); - collateralization.slash(id); + function testFail_SlashFromNonArbiter() public { + uint128 _expiration = uint128(block.timestamp) + 3; + address _arbiter = 0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF; + token.approve(address(collateralization), 1); + uint128 _id = collateralization.deposit(0, 1, _expiration, _arbiter); + vm.warp(_expiration - 1); + collateralization.slash(_id); } } From b5326a91f7d168a79c2809a9fa2316771b0d617c Mon Sep 17 00:00:00 2001 From: Theo Butler Date: Fri, 14 Jul 2023 13:39:22 -0400 Subject: [PATCH 03/23] Optimize redeposit state check --- src/Collateralization.sol | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Collateralization.sol b/src/Collateralization.sol index 1b2c579..bf94a8a 100644 --- a/src/Collateralization.sol +++ b/src/Collateralization.sol @@ -40,14 +40,12 @@ struct Deposit { /// │Unlocked│ │Locked│ │Withdrawn│ │Slashed│ /// └────────┘ └──────┘ └─────────┘ └───────┘ enum DepositState { - Unlocked, // 0x00 TODO: opt - Locked, // 0x01 - Withdrawn, // 0x10 - Slashed // 0x11 + Unlocked, // 0b00 + Locked, // 0b01 + Withdrawn, // 0b10 + Slashed // 0b11 } -uint8 constant _CanDeposit = 0x00; - /// Deposit in unexpected state. error UnexpectedState(DepositState state); /// Deposit value is zero. @@ -89,7 +87,8 @@ contract Collateralization { _id = lastID; } else { DepositState _state = getDeposit(_id).state; - if ((_state != DepositState.Withdrawn) && (_state != DepositState.Slashed)) revert UnexpectedState(_state); + // bit twiddling to save 19 gas + if ((uint8(_state) & 2) == 0) revert UnexpectedState(_state); } deposits[_id] = Deposit({ depositor: msg.sender, From 85a84f6f09c475ba4da5afddb28257b230948b32 Mon Sep 17 00:00:00 2001 From: Theo Butler Date: Fri, 14 Jul 2023 13:58:07 -0400 Subject: [PATCH 04/23] Add events --- src/Collateralization.sol | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Collateralization.sol b/src/Collateralization.sol index bf94a8a..65548c7 100644 --- a/src/Collateralization.sol +++ b/src/Collateralization.sol @@ -59,6 +59,11 @@ error NotFound(); /// This contract manages `Deposit`s as described above. contract Collateralization { + event _Deposit(uint128 indexed id, address indexed arbiter, uint256 value, uint128 expiration); + event _Lock(uint128 indexed id); + event _Withdraw(uint128 indexed id); + event _Slash(uint128 indexed id); + /// Burnable ERC-20 token held by this contract. ERC20Burnable public token; /// Mapping of deposit IDs to deposits. @@ -99,6 +104,7 @@ contract Collateralization { }); bool _transferSuccess = token.transferFrom(msg.sender, address(this), _value); require(_transferSuccess, "transfer failed"); + emit _Deposit(_id, _arbiter, _value, _expiration); return _id; } @@ -110,6 +116,7 @@ contract Collateralization { if (_deposit.state != DepositState.Unlocked) revert UnexpectedState(_deposit.state); if (block.timestamp >= _deposit.expiration) revert Expired(true); deposits[_id].state = DepositState.Locked; + emit _Lock(_id); } /// Unlock the deposit associated with the given ID and return its associated tokens to the depositor. @@ -125,6 +132,7 @@ contract Collateralization { deposits[_id].state = DepositState.Withdrawn; bool _transferSuccess = token.transfer(_deposit.depositor, _deposit.value); require(_transferSuccess, "transfer failed"); + emit _Withdraw(_id); } /// Remove a deposit prior to expiration and burn its associated tokens. This action can only be performed by the @@ -137,6 +145,7 @@ contract Collateralization { if (block.timestamp >= _deposit.expiration) revert Expired(true); deposits[_id].state = DepositState.Slashed; token.burn(_deposit.value); + emit _Slash(_id); } /// Return the deposit associated with the given ID. From ac788641c8f92114953333be37905053498036c4 Mon Sep 17 00:00:00 2001 From: Theo Butler Date: Fri, 14 Jul 2023 14:08:02 -0400 Subject: [PATCH 05/23] Restrict lock to arbiter --- src/Collateralization.sol | 1 + test/Collateralization.t.sol | 17 +++++++++-------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/Collateralization.sol b/src/Collateralization.sol index 65548c7..3927b5f 100644 --- a/src/Collateralization.sol +++ b/src/Collateralization.sol @@ -113,6 +113,7 @@ contract Collateralization { /// @param _id ID of the associated deposit. function lock(uint128 _id) public { Deposit memory _deposit = getDeposit(_id); + if (msg.sender != _deposit.arbiter) revert NotArbiter(); if (_deposit.state != DepositState.Unlocked) revert UnexpectedState(_deposit.state); if (block.timestamp >= _deposit.expiration) revert Expired(true); deposits[_id].state = DepositState.Locked; diff --git a/test/Collateralization.t.sol b/test/Collateralization.t.sol index ef798f8..ddcdca2 100644 --- a/test/Collateralization.t.sol +++ b/test/Collateralization.t.sol @@ -198,7 +198,7 @@ contract CollateralizationUnitTests is Test { function test_Lock() public { uint128 _expiration = uint128(block.timestamp) + 1; token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(0, 1, _expiration, address(0)); + uint128 _id = collateralization.deposit(0, 1, _expiration, address(this)); assertEq(uint256(collateralization.getDeposit(_id).state), uint256(DepositState.Unlocked)); collateralization.lock(_id); assertEq(uint256(collateralization.getDeposit(_id).state), uint256(DepositState.Locked)); @@ -208,7 +208,7 @@ contract CollateralizationUnitTests is Test { function testFail_LockAtExpiration() public { uint128 _expiration = uint128(block.timestamp) + 1; token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(0, 1, _expiration, address(0)); + uint128 _id = collateralization.deposit(0, 1, _expiration, address(this)); vm.warp(_expiration); collateralization.lock(_id); } @@ -216,7 +216,7 @@ contract CollateralizationUnitTests is Test { function testFail_LockAfterExpiration() public { uint128 _expiration = uint128(block.timestamp) + 1; token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(0, 1, _expiration, address(0)); + uint128 _id = collateralization.deposit(0, 1, _expiration, address(this)); vm.warp(_expiration + 1); collateralization.lock(_id); } @@ -229,7 +229,7 @@ contract CollateralizationUnitTests is Test { uint128 _expiration = uint128(block.timestamp) + 1; uint256 _initialBalance = token.balanceOf(address(this)); token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(0, 1, _expiration, address(0)); + uint128 _id = collateralization.deposit(0, 1, _expiration, address(this)); collateralization.lock(_id); vm.warp(_expiration); collateralization.withdraw(_id); @@ -242,7 +242,7 @@ contract CollateralizationUnitTests is Test { uint128 _expiration = uint128(block.timestamp) + 1; uint256 _initialBalance = token.balanceOf(address(this)); token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(0, 1, _expiration, address(0)); + uint128 _id = collateralization.deposit(0, 1, _expiration, address(this)); collateralization.lock(_id); vm.warp(_expiration + 1); collateralization.withdraw(_id); @@ -253,7 +253,7 @@ contract CollateralizationUnitTests is Test { function testFail_WithdrawBeforeExpiration() public { token.approve(address(collateralization), 1); uint128 _expiration = uint128(block.timestamp) + 3; - uint128 _id = collateralization.deposit(0, 1, _expiration, address(0)); + uint128 _id = collateralization.deposit(0, 1, _expiration, address(this)); collateralization.lock(_id); vm.warp(_expiration - 1); collateralization.withdraw(_id); @@ -276,7 +276,7 @@ contract CollateralizationUnitTests is Test { uint256 _initialBalance = token.balanceOf(address(this)); address _other = 0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF; token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(0, 1, _expiration, address(0)); + uint128 _id = collateralization.deposit(0, 1, _expiration, address(this)); collateralization.lock(_id); vm.warp(_expiration); vm.prank(_other); @@ -290,10 +290,11 @@ contract CollateralizationUnitTests is Test { uint256 _initialSupply = token.totalSupply(); token.approve(address(collateralization), 1); uint128 _id = collateralization.deposit(0, 1, _expiration, _arbiter); + vm.startPrank(_arbiter); collateralization.lock(_id); vm.warp(_expiration - 1); - vm.prank(_arbiter); collateralization.slash(_id); + vm.stopPrank(); assertEq(token.totalSupply(), _initialSupply - 1); assertEq(uint256(collateralization.getDeposit(_id).state), uint256(DepositState.Slashed)); } From b5808e674221325349e2f2056e03d8ed48864ad5 Mon Sep 17 00:00:00 2001 From: Theo Butler Date: Fri, 14 Jul 2023 19:11:18 -0400 Subject: [PATCH 06/23] Remove deposit state reuse --- src/Collateralization.sol | 22 +++--------- test/Collateralization.t.sol | 68 ++++++++++-------------------------- 2 files changed, 23 insertions(+), 67 deletions(-) diff --git a/src/Collateralization.sol b/src/Collateralization.sol index 3927b5f..b688c59 100644 --- a/src/Collateralization.sol +++ b/src/Collateralization.sol @@ -20,7 +20,7 @@ struct Deposit { /// │Unlocked│ │Locked│ │Withdrawn│ │Slashed│ /// └───┬────┘ └──┬───┘ └────┬────┘ └───┬───┘ /// │ lock │ │ │ -/// │ ─────────────────> │ │ +/// │ ────────────────>│ │ │ /// │ │ │ │ /// │ │ withdraw │ │ /// │ │ ─────────────────>│ │ @@ -30,12 +30,6 @@ struct Deposit { /// │ │ │ │ /// │ │ slash │ /// │ │ ─────────────────────────────────────>│ -/// │ │ │ │ -/// │ deposit │ │ -/// │ <────────────────────────────────────│ │ -/// │ │ │ │ -/// │ │ deposit │ │ -/// │ <────────────────────────────────────────────────────────│ /// ┌───┴────┐ ┌──┴───┐ ┌────┴────┐ ┌───┴───┐ /// │Unlocked│ │Locked│ │Withdrawn│ │Slashed│ /// └────────┘ └──────┘ └─────────┘ └───────┘ @@ -78,23 +72,15 @@ contract Collateralization { } /// Create a new deposit, returning its associated ID. - /// @param _id ID of the deposit ID to reuse. This should be set to zero to receive a new ID. IDs may only be reused - /// if the associated deposit is withdrawn or slashed. /// @param _value Token value of the new deposit. /// @param _expiration Expiration timestamp of the new deposit, in seconds. /// @param _arbiter Arbiter of the new deposit. /// @return id Unique ID associated with the new deposit. - function deposit(uint128 _id, uint256 _value, uint128 _expiration, address _arbiter) public returns (uint128) { + function deposit(uint256 _value, uint128 _expiration, address _arbiter) public returns (uint128) { if (_value == 0) revert ZeroValue(); if (block.timestamp >= _expiration) revert Expired(true); - if (_id == 0) { - lastID += 1; - _id = lastID; - } else { - DepositState _state = getDeposit(_id).state; - // bit twiddling to save 19 gas - if ((uint8(_state) & 2) == 0) revert UnexpectedState(_state); - } + lastID += 1; + uint128 _id = lastID; deposits[_id] = Deposit({ depositor: msg.sender, arbiter: _arbiter, diff --git a/test/Collateralization.t.sol b/test/Collateralization.t.sol index ddcdca2..1a744f5 100644 --- a/test/Collateralization.t.sol +++ b/test/Collateralization.t.sol @@ -44,7 +44,7 @@ contract CollateralizationHandler is CommonBase, StdUtils { uint256 _value = bound(__value, 1, collateralization.token().balanceOf(_depositor)); vm.startPrank(_depositor); collateralization.token().approve(address(collateralization), _value); - uint128 _id = collateralization.deposit(0, _value, _genExpiration(__expiration), _genActor(__arbiter)); + uint128 _id = collateralization.deposit(_value, _genExpiration(__expiration), _genActor(__arbiter)); vm.stopPrank(); depositIDs.push(_id); return _id; @@ -137,7 +137,7 @@ contract CollateralizationUnitTests is Test { uint128 _expiration = uint128(block.timestamp) + 1; uint256 _initialBalance = token.balanceOf(address(this)); token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(0, 1, _expiration, address(0)); + uint128 _id = collateralization.deposit(1, _expiration, address(0)); assertEq(token.balanceOf(address(this)), _initialBalance - 1); assertEq(token.balanceOf(address(collateralization)), 1); assertEq(collateralization.getDeposit(_id).depositor, address(this)); @@ -148,57 +148,27 @@ contract CollateralizationUnitTests is Test { function test_DepositUniqueID() public { uint128 _expiration = uint128(block.timestamp) + 1; token.approve(address(collateralization), 2); - uint128 _id1 = collateralization.deposit(0, 1, _expiration, address(0)); - uint128 _id2 = collateralization.deposit(0, 1, _expiration, address(0)); + uint128 _id1 = collateralization.deposit(1, _expiration, address(0)); + uint128 _id2 = collateralization.deposit(1, _expiration, address(0)); assertNotEq(_id1, _id2); } function testFail_DepositExpirationAtBlock() public { uint128 _expiration = uint128(block.timestamp); token.approve(address(collateralization), 1); - collateralization.deposit(0, 1, _expiration, address(0)); + collateralization.deposit(1, _expiration, address(0)); } function testFail_DepositExpirationBeforeBlock() public { uint128 _expiration = uint128(block.timestamp) - 1; token.approve(address(collateralization), 1); - collateralization.deposit(0, 1, _expiration, address(0)); - } - - function testFail_DepositReuseLocked() public { - uint128 _expiration = uint128(block.timestamp) + 1; - token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(0, 1, _expiration, address(0)); - collateralization.lock(_id); - token.approve(address(collateralization), 1); - collateralization.deposit(_id, 1, _expiration, address(0)); - } - - function test_DepositReuseAfterWithdraw() public { - uint128 _expiration = uint128(block.timestamp) + 1; - token.approve(address(collateralization), 1); - uint128 _id1 = collateralization.deposit(0, 1, _expiration, address(0)); - collateralization.withdraw(_id1); - token.approve(address(collateralization), 1); - uint128 _id2 = collateralization.deposit(_id1, 1, _expiration, address(0)); - assertEq(_id1, _id2); - } - - function test_DepositReuseAfterSlash() public { - uint128 _expiration = uint128(block.timestamp) + 1; - token.approve(address(collateralization), 1); - uint128 _id1 = collateralization.deposit(0, 1, _expiration, address(this)); - collateralization.lock(_id1); - collateralization.slash(_id1); - token.approve(address(collateralization), 1); - uint128 _id2 = collateralization.deposit(_id1, 1, _expiration, address(0)); - assertEq(_id1, _id2); + collateralization.deposit(1, _expiration, address(0)); } function test_Lock() public { uint128 _expiration = uint128(block.timestamp) + 1; token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(0, 1, _expiration, address(this)); + uint128 _id = collateralization.deposit(1, _expiration, address(this)); assertEq(uint256(collateralization.getDeposit(_id).state), uint256(DepositState.Unlocked)); collateralization.lock(_id); assertEq(uint256(collateralization.getDeposit(_id).state), uint256(DepositState.Locked)); @@ -208,7 +178,7 @@ contract CollateralizationUnitTests is Test { function testFail_LockAtExpiration() public { uint128 _expiration = uint128(block.timestamp) + 1; token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(0, 1, _expiration, address(this)); + uint128 _id = collateralization.deposit(1, _expiration, address(this)); vm.warp(_expiration); collateralization.lock(_id); } @@ -216,7 +186,7 @@ contract CollateralizationUnitTests is Test { function testFail_LockAfterExpiration() public { uint128 _expiration = uint128(block.timestamp) + 1; token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(0, 1, _expiration, address(this)); + uint128 _id = collateralization.deposit(1, _expiration, address(this)); vm.warp(_expiration + 1); collateralization.lock(_id); } @@ -229,7 +199,7 @@ contract CollateralizationUnitTests is Test { uint128 _expiration = uint128(block.timestamp) + 1; uint256 _initialBalance = token.balanceOf(address(this)); token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(0, 1, _expiration, address(this)); + uint128 _id = collateralization.deposit(1, _expiration, address(this)); collateralization.lock(_id); vm.warp(_expiration); collateralization.withdraw(_id); @@ -242,7 +212,7 @@ contract CollateralizationUnitTests is Test { uint128 _expiration = uint128(block.timestamp) + 1; uint256 _initialBalance = token.balanceOf(address(this)); token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(0, 1, _expiration, address(this)); + uint128 _id = collateralization.deposit(1, _expiration, address(this)); collateralization.lock(_id); vm.warp(_expiration + 1); collateralization.withdraw(_id); @@ -253,7 +223,7 @@ contract CollateralizationUnitTests is Test { function testFail_WithdrawBeforeExpiration() public { token.approve(address(collateralization), 1); uint128 _expiration = uint128(block.timestamp) + 3; - uint128 _id = collateralization.deposit(0, 1, _expiration, address(this)); + uint128 _id = collateralization.deposit(1, _expiration, address(this)); collateralization.lock(_id); vm.warp(_expiration - 1); collateralization.withdraw(_id); @@ -263,7 +233,7 @@ contract CollateralizationUnitTests is Test { uint128 _expiration = uint128(block.timestamp) + 1; uint256 _initialBalance = token.balanceOf(address(this)); token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(0, 1, _expiration, address(this)); + uint128 _id = collateralization.deposit(1, _expiration, address(this)); collateralization.lock(_id); vm.warp(_expiration); collateralization.withdraw(_id); @@ -276,7 +246,7 @@ contract CollateralizationUnitTests is Test { uint256 _initialBalance = token.balanceOf(address(this)); address _other = 0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF; token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(0, 1, _expiration, address(this)); + uint128 _id = collateralization.deposit(1, _expiration, address(this)); collateralization.lock(_id); vm.warp(_expiration); vm.prank(_other); @@ -289,7 +259,7 @@ contract CollateralizationUnitTests is Test { address _arbiter = 0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF; uint256 _initialSupply = token.totalSupply(); token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(0, 1, _expiration, _arbiter); + uint128 _id = collateralization.deposit(1, _expiration, _arbiter); vm.startPrank(_arbiter); collateralization.lock(_id); vm.warp(_expiration - 1); @@ -302,7 +272,7 @@ contract CollateralizationUnitTests is Test { function testFail_SlashAtExpiration() public { uint128 _expiration = uint128(block.timestamp) + 3; token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(0, 1, _expiration, address(this)); + uint128 _id = collateralization.deposit(1, _expiration, address(this)); collateralization.lock(_id); vm.warp(_expiration); collateralization.slash(_id); @@ -311,7 +281,7 @@ contract CollateralizationUnitTests is Test { function testFail_SlashAfterExpiration() public { uint128 _expiration = uint128(block.timestamp) + 3; token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(0, 1, _expiration, address(this)); + uint128 _id = collateralization.deposit(1, _expiration, address(this)); collateralization.lock(_id); vm.warp(_expiration + 1); collateralization.slash(_id); @@ -320,7 +290,7 @@ contract CollateralizationUnitTests is Test { function testFail_SlashUnlocked() public { uint128 _expiration = uint128(block.timestamp) + 3; token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(0, 1, _expiration, address(this)); + uint128 _id = collateralization.deposit(1, _expiration, address(this)); vm.warp(_expiration + 1); collateralization.slash(_id); } @@ -329,7 +299,7 @@ contract CollateralizationUnitTests is Test { uint128 _expiration = uint128(block.timestamp) + 3; address _arbiter = 0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF; token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(0, 1, _expiration, _arbiter); + uint128 _id = collateralization.deposit(1, _expiration, _arbiter); vm.warp(_expiration - 1); collateralization.slash(_id); } From e01b029f8af8c49fdfef42abea7d58ce8b5ae55d Mon Sep 17 00:00:00 2001 From: Theo Butler Date: Fri, 14 Jul 2023 19:00:41 -0400 Subject: [PATCH 07/23] Add examples --- src/examples/DataService.sol | 64 ++++++++++++++++++++++ src/examples/Lender.sol | 61 +++++++++++++++++++++ src/examples/LoanAggregator.sol | 74 +++++++++++++++++++++++++ test/Example.t.sol | 95 +++++++++++++++++++++++++++++++++ 4 files changed, 294 insertions(+) create mode 100644 src/examples/DataService.sol create mode 100644 src/examples/Lender.sol create mode 100644 src/examples/LoanAggregator.sol create mode 100644 test/Example.t.sol diff --git a/src/examples/DataService.sol b/src/examples/DataService.sol new file mode 100644 index 0000000..b3dba3d --- /dev/null +++ b/src/examples/DataService.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol"; +import {Collateralization, Deposit} from "../Collateralization.sol"; +import {IDataService} from "./LoanAggregator.sol"; + +contract DataService is Ownable, IDataService { + struct ProviderState { + uint128 deposit; + uint128 payment; + } + + Collateralization public collateralization; + mapping(address => ProviderState) public providers; + uint128 public disputePeriod; + + constructor(Collateralization _collateralization, uint128 _disputePeriod) { + collateralization = _collateralization; + disputePeriod = _disputePeriod; + } + + /// Add provider and fund their future payment. + function addProvider(address _provider, uint128 _payment) public onlyOwner { + require(_payment > 0); + require(providers[_provider].payment == 0, "provider exists"); + providers[_provider] = ProviderState({deposit: 0, payment: _payment}); + collateralization.token().transferFrom(msg.sender, address(this), _payment); + } + + function removeProvider(address _provider) public onlyOwner { + ProviderState memory _state = getProviderState(_provider); + require(_state.deposit == 0, "payment already made"); + delete providers[_provider]; + } + + /// Remove the provider and slash their deposit. + function slash(address _provider) public onlyOwner { + ProviderState memory _state = getProviderState(_provider); + delete providers[_provider]; + collateralization.slash(_state.deposit); + } + + /// Called by data service provider to receive payment. This locks the given deposit to begin a dispute period. + function remitPayment(address _providerAddr, uint128 _depositID) public { + ProviderState memory _provider = getProviderState(_providerAddr); + Deposit memory _deposit = collateralization.getDeposit(_depositID); + + uint256 minCollateral = uint256(_provider.payment) * 10; + require(_deposit.value >= minCollateral, "collateral below minimum"); + uint128 disputePeriodEnd = uint128(block.timestamp + disputePeriod); + require(_deposit.expiration >= disputePeriodEnd, "collateral expiration before end of dispute period"); + + providers[_providerAddr].deposit = _depositID; + collateralization.lock(_depositID); + collateralization.token().transfer(_providerAddr, _provider.payment); + } + + function getProviderState(address _provider) public view returns (ProviderState memory) { + ProviderState memory _state = providers[_provider]; + require(_state.payment != 0, "provider not found"); + return _state; + } +} diff --git a/src/examples/Lender.sol b/src/examples/Lender.sol new file mode 100644 index 0000000..b62bdfb --- /dev/null +++ b/src/examples/Lender.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol"; +import {SafeCast} from "openzeppelin-contracts/contracts/utils/math/SafeCast.sol"; +import {AggregatedLoan, LoanAggregator, LoanCommitment} from "./LoanAggregator.sol"; + +struct Limits { + uint256 maxValue; + uint64 maxDuration; +} + +contract Lender is Ownable { + LoanAggregator public agg; + Limits public limits; + + constructor(LoanAggregator _agg, Limits memory _limits) { + agg = _agg; + limits = _limits; + } + + function collect(uint256 _amount) public onlyOwner returns (bool) { + return agg.collateralization().token().transfer(owner(), _amount); + } + + function borrow(uint256 _value, uint256 _collateral, uint256 _payment, uint128 _expiration) + public + returns (LoanCommitment memory) + { + require(_collateral <= _value, "collateral > value"); + uint64 _duration = SafeCast.toUint64(_expiration - block.timestamp); + require(_duration <= limits.maxDuration, "duration over maximum"); + require(_value <= limits.maxValue, "value over maximum"); + require(_payment >= expectedPayment(_value, _duration), "payment below expected"); + uint256 _transferAmount = _collateral + _payment; + agg.collateralization().token().transferFrom(msg.sender, address(this), _transferAmount); + + agg.collateralization().token().approve(address(agg), _value); + return LoanCommitment({ + loan: AggregatedLoan({ + lender: address(this), + value: _value, + borrower: msg.sender, + borrowerCollateral: _collateral + }), + signature: "siggy" + }); + } + + /// Return the expected payment for a loan, based on its value and duration. + /// @param _value Deposit value. + /// @param _duration Deposit duration from the block at which the deposit is funded, in seconds. + function expectedPayment(uint256 _value, uint64 _duration) public view returns (uint256) { + // TODO: Now for the tricky bit! + // Ideally the owner would be able to set an expected annualized return. However, I did not + // figure out an obvious way to calculate the annualized return given the loan duration + // using standard Solidity math operations or those provided by OpenZeppelin. And using a + // table would not be ideal, since storage space is more difficult to justify than compute. + return 1; + } +} diff --git a/src/examples/LoanAggregator.sol b/src/examples/LoanAggregator.sol new file mode 100644 index 0000000..f77e513 --- /dev/null +++ b/src/examples/LoanAggregator.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {Collateralization, Deposit, DepositState} from "../Collateralization.sol"; + +interface IDataService { + function remitPayment(address _provider, uint128 _deposit) external; +} + +struct LoanCommitment { + AggregatedLoan loan; + bytes signature; +} + +struct AggregatedLoan { + address lender; + uint256 value; + address borrower; + uint256 borrowerCollateral; +} + +contract LoanAggregator { + Collateralization public collateralization; + mapping(uint128 => AggregatedLoan[]) public loans; + + constructor(Collateralization _collateralization) { + collateralization = _collateralization; + } + + function remitPayment(IDataService _arbiter, uint128 _expiration, LoanCommitment[] calldata _loanCommitments) + public + returns (uint128) + { + uint256 _index = 0; + uint256 _value = 0; + while (_index < _loanCommitments.length) { + LoanCommitment memory _commitment = _loanCommitments[_index]; + require(_commitment.loan.borrowerCollateral < _commitment.loan.value); + // TODO: verify signature of (lender, value, arbiter, expiration) + _value += _commitment.loan.value; + collateralization.token().transferFrom(_commitment.loan.lender, address(this), _commitment.loan.value); + _index += 1; + } + collateralization.token().approve(address(collateralization), _value); + uint128 _deposit = collateralization.deposit(_value, _expiration, address(_arbiter)); + _index = 0; + while (_index < _loanCommitments.length) { + loans[_deposit].push(_loanCommitments[_index].loan); + _index += 1; + } + _arbiter.remitPayment(msg.sender, _deposit); + return _deposit; + } + + function withdraw(uint128 _depositID) public { + // Note that this sort of check prevents the collateralization contract from reusing the storage for deposit + // IDs. Since that would result in ABA-style problems. Alternatively we could allow reuse, but restrict + // withdraw to only the depositor. + Deposit memory _deposit = collateralization.getDeposit(_depositID); + if (_deposit.state != DepositState.Withdrawn) { + collateralization.withdraw(_depositID); + } + uint256 _index = 0; + while (_index < loans[_depositID].length) { + AggregatedLoan memory _loan = loans[_depositID][_index]; + uint256 _borrowerReturn = _loan.borrowerCollateral; + uint256 _lenderReturn = _loan.value - _loan.borrowerCollateral; + collateralization.token().transfer(_loan.lender, _lenderReturn); + collateralization.token().transfer(_loan.borrower, _borrowerReturn); + _index += 1; + } + delete loans[_depositID]; + } +} diff --git a/test/Example.t.sol b/test/Example.t.sol new file mode 100644 index 0000000..0d6b8fc --- /dev/null +++ b/test/Example.t.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {Test} from "forge-std/Test.sol"; +import {ERC20Burnable} from "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.sol"; +import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; +import {Collateralization, Deposit, DepositState, UnexpectedState} from "../src/Collateralization.sol"; +import {DataService} from "../src/examples/DataService.sol"; +import {Lender, Limits} from "../src/examples/Lender.sol"; +import {AggregatedLoan, IDataService, LoanAggregator, LoanCommitment} from "../src/examples/LoanAggregator.sol"; + +contract TestToken is ERC20Burnable { + constructor(uint256 _initialSupply) ERC20("MockCoin", "MOCK") { + _mint(msg.sender, _initialSupply); + } +} + +contract CollateralizationUnitTests is Test { + TestToken public token; + Collateralization public collateralization; + + function setUp() public { + token = new TestToken(1_000); + collateralization = new Collateralization(token); + } + + function test_Example() public { + DataService _dataService = new DataService(collateralization, 20 days); + LoanAggregator _agg = new LoanAggregator(collateralization); + Lender _lender = new Lender(_agg, Limits({maxValue: 100, maxDuration: 30 days})); + token.transfer(address(_lender), 80); + + token.approve(address(_dataService), 10); + _dataService.addProvider(address(this), 10); + + uint256 _initialBalance = token.balanceOf(address(this)); + uint256 _initialLenderBalance = token.balanceOf(address(_lender)); + + LoanCommitment[] memory _loanCommitments = new LoanCommitment[](2); + token.approve(address(_agg), 20); + _loanCommitments[0] = LoanCommitment({ + loan: AggregatedLoan({lender: address(this), value: 20, borrower: address(this), borrowerCollateral: 0}), + signature: "siggy" + }); + token.approve(address(_lender), 6); + _loanCommitments[1] = _lender.borrow(80, 5, 1, _dataService.disputePeriod()); + + uint128 _expiration = uint128(block.timestamp) + _dataService.disputePeriod(); + uint128 _deposit = _agg.remitPayment(DataService(_dataService), _expiration, _loanCommitments); + + assertEq(token.balanceOf(address(this)), _initialBalance + 10 - 26); + assertEq(token.balanceOf(address(_lender)), _initialLenderBalance - 80 + 6); + + vm.warp(block.number + _dataService.disputePeriod()); + _agg.withdraw(_deposit); + + assertEq(token.balanceOf(address(this)), _initialBalance + 10 - 1); + assertEq(token.balanceOf(address(_lender)), _initialLenderBalance + 1); + } + + function test_ExampleSlash() public { + DataService _dataService = new DataService(collateralization, 20 days); + LoanAggregator _agg = new LoanAggregator(collateralization); + Lender _lender = new Lender(_agg, Limits({maxValue: 100, maxDuration: 30 days})); + token.transfer(address(_lender), 80); + + token.approve(address(_dataService), 10); + _dataService.addProvider(address(this), 10); + + uint256 _initialBalance = token.balanceOf(address(this)); + uint256 _initialLenderBalance = token.balanceOf(address(_lender)); + + LoanCommitment[] memory _loanCommitments = new LoanCommitment[](2); + token.approve(address(_agg), 20); + _loanCommitments[0] = LoanCommitment({ + loan: AggregatedLoan({lender: address(this), value: 20, borrower: address(this), borrowerCollateral: 0}), + signature: "siggy" + }); + token.approve(address(_lender), 6); + _loanCommitments[1] = _lender.borrow(80, 5, 1, _dataService.disputePeriod()); + + uint128 _expiration = uint128(block.timestamp) + _dataService.disputePeriod(); + uint128 _deposit = _agg.remitPayment(DataService(_dataService), _expiration, _loanCommitments); + + assertEq(token.balanceOf(address(this)), _initialBalance + 10 - 26); + assertEq(token.balanceOf(address(_lender)), _initialLenderBalance - 80 + 6); + + vm.warp(block.number + _dataService.disputePeriod() - 1); + _dataService.slash(address(this)); + + vm.warp(block.number + _dataService.disputePeriod()); + vm.expectRevert(abi.encodeWithSelector(UnexpectedState.selector, DepositState.Slashed)); + _agg.withdraw(_deposit); + } +} From f58cf61b90b9e8eac43b9a6082db94f6afea22a4 Mon Sep 17 00:00:00 2001 From: Theo Butler Date: Tue, 18 Jul 2023 00:12:38 -0400 Subject: [PATCH 08/23] Allow partial slash --- src/Collateralization.sol | 81 ++++++++++++++---------- src/examples/DataService.sol | 7 +- src/examples/Lender.sol | 29 ++++++--- src/examples/LoanAggregator.sol | 36 ++++++----- test/Collateralization.t.sol | 28 +++++--- test/Example.t.sol | 109 +++++++++++++++++--------------- 6 files changed, 169 insertions(+), 121 deletions(-) diff --git a/src/Collateralization.sol b/src/Collateralization.sol index b688c59..f8ee6ca 100644 --- a/src/Collateralization.sol +++ b/src/Collateralization.sol @@ -3,11 +3,12 @@ pragma solidity ^0.8.13; import "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.sol"; -/// A `Deposit` describes a slashable, time-locked deposit of `value` tokens. A `Deposit` must be `locked` to provide -/// the following invariants: -/// - A `Deposit` may only be withdrawn when `block.timestamp >= expiration`. Withdrawal returns `value` tokens to the +/// A Deposit describes a slashable, time-locked deposit of `value` tokens. A Deposit must be in the locked `state` to +/// provide the following invariants: +/// - The `arbiter` has the authority to slash the Deposit before `expiration`, which burns a given amount tokens. A +/// slash also reduces the tokens eventually available to withdraw by the same amount. +/// - A Deposit may only be withdrawn when `block.timestamp >= expiration`. Withdrawal returns `value` tokens to the /// depositor. -/// - The `arbiter` has the authority to slash the `Deposit` before `expiration`, which also burns `value` tokens. struct Deposit { address depositor; address arbiter; @@ -16,28 +17,31 @@ struct Deposit { DepositState state; } -/// ┌────────┐ ┌──────┐ ┌─────────┐ ┌───────┐ -/// │Unlocked│ │Locked│ │Withdrawn│ │Slashed│ -/// └───┬────┘ └──┬───┘ └────┬────┘ └───┬───┘ -/// │ lock │ │ │ -/// │ ────────────────>│ │ │ -/// │ │ │ │ -/// │ │ withdraw │ │ -/// │ │ ─────────────────>│ │ -/// │ │ │ │ -/// │ withdraw │ │ -/// │ ────────────────────────────────────>│ │ -/// │ │ │ │ -/// │ │ slash │ -/// │ │ ─────────────────────────────────────>│ -/// ┌───┴────┐ ┌──┴───┐ ┌────┴────┐ ┌───┴───┐ -/// │Unlocked│ │Locked│ │Withdrawn│ │Slashed│ -/// └────────┘ └──────┘ └─────────┘ └───────┘ +/// ┌────────┐ ┌──────┐ ┌─────────┐ +/// │Unlocked│ │Locked│ │Withdrawn│ +/// └───┬────┘ └──┬───┘ └────┬────┘ +/// │ lock │ │ +/// │ ─────────────────> │ +/// │ │ │ +/// │ │────┐ │ +/// │ │ │ slash │ +/// │ │<───┘ │ +/// │ │ │ +/// │ │ withdraw │ +/// │ │ ─────────────────>│ +/// │ │ │ +/// │ withdraw │ +/// │ ────────────────────────────────────>│ +/// │ │ │ +/// │ deposit │ +/// │ <────────────────────────────────────│ +/// ┌───┴────┐ ┌──┴───┐ ┌────┴────┐ +/// │Unlocked│ │Locked│ │Withdrawn│ +/// └────────┘ └──────┘ └─────────┘ enum DepositState { - Unlocked, // 0b00 - Locked, // 0b01 - Withdrawn, // 0b10 - Slashed // 0b11 + Unlocked, + Locked, + Withdrawn } /// Deposit in unexpected state. @@ -46,17 +50,21 @@ error UnexpectedState(DepositState state); error ZeroValue(); /// Deposit expiration in unexpected state. error Expired(bool expired); +/// Withdraw called by an address that isn't the depositor. +error NotDepositor(); /// Slash called by an address that isn't the deposit's arbiter. error NotArbiter(); /// Deposit does not exist. error NotFound(); +/// Slash amount is larger than remainning deposit balance. +error SlashAmountTooLarge(); -/// This contract manages `Deposit`s as described above. +/// This contract manages Deposits as described above. contract Collateralization { event _Deposit(uint128 indexed id, address indexed arbiter, uint256 value, uint128 expiration); event _Lock(uint128 indexed id); event _Withdraw(uint128 indexed id); - event _Slash(uint128 indexed id); + event _Slash(uint128 indexed id, uint256 amount); /// Burnable ERC-20 token held by this contract. ERC20Burnable public token; @@ -77,6 +85,7 @@ contract Collateralization { /// @param _arbiter Arbiter of the new deposit. /// @return id Unique ID associated with the new deposit. function deposit(uint256 _value, uint128 _expiration, address _arbiter) public returns (uint128) { + // TODO: reuse state when given `(_id != 0) && (msg.sender == getDeposit(_id).depositor)` if (_value == 0) revert ZeroValue(); if (block.timestamp >= _expiration) revert Expired(true); lastID += 1; @@ -110,6 +119,7 @@ contract Collateralization { /// @param _id ID of the associated deposit. function withdraw(uint128 _id) public { Deposit memory _deposit = getDeposit(_id); + if (_deposit.depositor != msg.sender) revert NotDepositor(); DepositState _state = deposits[_id].state; if (_state == DepositState.Locked) { if (block.timestamp < _deposit.expiration) revert Expired(false); @@ -122,24 +132,26 @@ contract Collateralization { emit _Withdraw(_id); } - /// Remove a deposit prior to expiration and burn its associated tokens. This action can only be performed by the - /// arbiter of the deposit associated with the given ID. + /// Burn some amount of the deposit value prior to expiration. This action can only be performed by the arbiter of + /// the deposit associated with the given ID. /// @param _id ID of the associated deposit. - function slash(uint128 _id) public { + /// @param _amount Amount of remaining tokens to burn. + function slash(uint128 _id, uint256 _amount) public { Deposit memory _deposit = getDeposit(_id); if (msg.sender != _deposit.arbiter) revert NotArbiter(); if (_deposit.state != DepositState.Locked) revert UnexpectedState(_deposit.state); if (block.timestamp >= _deposit.expiration) revert Expired(true); - deposits[_id].state = DepositState.Slashed; - token.burn(_deposit.value); - emit _Slash(_id); + if (_amount > _deposit.value) revert SlashAmountTooLarge(); + deposits[_id].value -= _amount; + token.burn(_amount); + emit _Slash(_id, _amount); } /// Return the deposit associated with the given ID. /// @param _id ID of the associated deposit. function getDeposit(uint128 _id) public view returns (Deposit memory) { Deposit memory _deposit = deposits[_id]; - if (_deposit.value == 0) revert NotFound(); + if (_deposit.depositor == address(0)) revert NotFound(); return _deposit; } @@ -148,6 +160,7 @@ contract Collateralization { /// @param _id ID of the associated deposit. function isSlashable(uint128 _id) public view returns (bool) { Deposit memory _deposit = getDeposit(_id); + // TODO: also check if `_deposit.value > 0`? return (_deposit.state == DepositState.Locked) && (block.timestamp < _deposit.expiration); } } diff --git a/src/examples/DataService.sol b/src/examples/DataService.sol index b3dba3d..1d036c9 100644 --- a/src/examples/DataService.sol +++ b/src/examples/DataService.sol @@ -34,11 +34,10 @@ contract DataService is Ownable, IDataService { delete providers[_provider]; } - /// Remove the provider and slash their deposit. - function slash(address _provider) public onlyOwner { + /// Slash the provider's deposit. + function slash(address _provider, uint256 _amount) public onlyOwner { ProviderState memory _state = getProviderState(_provider); - delete providers[_provider]; - collateralization.slash(_state.deposit); + collateralization.slash(_state.deposit, _amount); } /// Called by data service provider to receive payment. This locks the given deposit to begin a dispute period. diff --git a/src/examples/Lender.sol b/src/examples/Lender.sol index b62bdfb..dece8f0 100644 --- a/src/examples/Lender.sol +++ b/src/examples/Lender.sol @@ -3,16 +3,23 @@ pragma solidity ^0.8.13; import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol"; import {SafeCast} from "openzeppelin-contracts/contracts/utils/math/SafeCast.sol"; -import {AggregatedLoan, LoanAggregator, LoanCommitment} from "./LoanAggregator.sol"; +import {AggregatedLoan, ILender, LoanAggregator, LoanCommitment} from "./LoanAggregator.sol"; struct Limits { uint256 maxValue; uint64 maxDuration; } -contract Lender is Ownable { +contract Lender is Ownable, ILender { + struct LoanState { + address borrower; + uint256 initialValue; + uint256 borrowerCollateral; + } + LoanAggregator public agg; Limits public limits; + LoanState[] public loans; constructor(LoanAggregator _agg, Limits memory _limits) { agg = _agg; @@ -35,14 +42,11 @@ contract Lender is Ownable { uint256 _transferAmount = _collateral + _payment; agg.collateralization().token().transferFrom(msg.sender, address(this), _transferAmount); + uint96 _loanIndex = uint96(loans.length); + loans.push(LoanState({borrower: msg.sender, initialValue: _value, borrowerCollateral: _collateral})); agg.collateralization().token().approve(address(agg), _value); return LoanCommitment({ - loan: AggregatedLoan({ - lender: address(this), - value: _value, - borrower: msg.sender, - borrowerCollateral: _collateral - }), + loan: AggregatedLoan({lender: this, lenderData: _loanIndex, value: _value}), signature: "siggy" }); } @@ -58,4 +62,13 @@ contract Lender is Ownable { // table would not be ideal, since storage space is more difficult to justify than compute. return 1; } + + function onCollateralWithraw(uint256 _value, uint96 _lenderData) public { + LoanState memory _loan = loans[_lenderData]; + delete loans[_lenderData]; + uint256 _loss = _loan.initialValue - _value; + if (_loss < _loan.borrowerCollateral) { + agg.collateralization().token().transfer(_loan.borrower, _loan.borrowerCollateral - _loss); + } + } } diff --git a/src/examples/LoanAggregator.sol b/src/examples/LoanAggregator.sol index f77e513..4189c51 100644 --- a/src/examples/LoanAggregator.sol +++ b/src/examples/LoanAggregator.sol @@ -7,16 +7,19 @@ interface IDataService { function remitPayment(address _provider, uint128 _deposit) external; } +interface ILender { + function onCollateralWithraw(uint256 _value, uint96 _lenderData) external; +} + struct LoanCommitment { AggregatedLoan loan; bytes signature; } struct AggregatedLoan { - address lender; + ILender lender; + uint96 lenderData; uint256 value; - address borrower; - uint256 borrowerCollateral; } contract LoanAggregator { @@ -35,10 +38,11 @@ contract LoanAggregator { uint256 _value = 0; while (_index < _loanCommitments.length) { LoanCommitment memory _commitment = _loanCommitments[_index]; - require(_commitment.loan.borrowerCollateral < _commitment.loan.value); // TODO: verify signature of (lender, value, arbiter, expiration) _value += _commitment.loan.value; - collateralization.token().transferFrom(_commitment.loan.lender, address(this), _commitment.loan.value); + collateralization.token().transferFrom( + address(_commitment.loan.lender), address(this), _commitment.loan.value + ); _index += 1; } collateralization.token().approve(address(collateralization), _value); @@ -53,20 +57,22 @@ contract LoanAggregator { } function withdraw(uint128 _depositID) public { - // Note that this sort of check prevents the collateralization contract from reusing the storage for deposit - // IDs. Since that would result in ABA-style problems. Alternatively we could allow reuse, but restrict - // withdraw to only the depositor. Deposit memory _deposit = collateralization.getDeposit(_depositID); - if (_deposit.state != DepositState.Withdrawn) { - collateralization.withdraw(_depositID); - } + collateralization.withdraw(_depositID); + // calculate original deposit value uint256 _index = 0; + uint256 _initialValue = 0; + while (_index < loans[_depositID].length) { + _initialValue += loans[_depositID][_index].value; + _index += 1; + } + // distribute remaining deposit value back to lenders + _index = 0; while (_index < loans[_depositID].length) { AggregatedLoan memory _loan = loans[_depositID][_index]; - uint256 _borrowerReturn = _loan.borrowerCollateral; - uint256 _lenderReturn = _loan.value - _loan.borrowerCollateral; - collateralization.token().transfer(_loan.lender, _lenderReturn); - collateralization.token().transfer(_loan.borrower, _borrowerReturn); + uint256 _lenderReturn = (_loan.value * _deposit.value) / _initialValue; + collateralization.token().transfer(address(_loan.lender), _lenderReturn); + _loan.lender.onCollateralWithraw(_lenderReturn, _loan.lenderData); _index += 1; } delete loans[_depositID]; diff --git a/test/Collateralization.t.sol b/test/Collateralization.t.sol index 1a744f5..1edc2f4 100644 --- a/test/Collateralization.t.sol +++ b/test/Collateralization.t.sol @@ -62,10 +62,10 @@ contract CollateralizationHandler is CommonBase, StdUtils { _removeDepositID(_id); } - function slash(uint256 __sender, uint256 __id) public { + function slash(uint256 __sender, uint256 __id, uint256 __amount) public { uint128 _id = _genID(__id); vm.prank(_genActor(__sender)); - collateralization.slash(_id); + collateralization.slash(_id, bound(__amount, 0, collateralization.getDeposit(_id).value)); assert(collateralization.isSlashable(_id)); _removeDepositID(_id); } @@ -241,7 +241,17 @@ contract CollateralizationUnitTests is Test { assertEq(token.balanceOf(address(collateralization)), 0); } - function test_WithdrawFromNonDepositor() public { + function testFail_WithdrawTwice() public { + uint128 _expiration = uint128(block.timestamp) + 1; + token.approve(address(collateralization), 2); + uint128 _id = collateralization.deposit(2, _expiration, address(this)); + collateralization.lock(_id); + vm.warp(_expiration); + collateralization.withdraw(_id); + collateralization.withdraw(_id); + } + + function testFail_WithdrawFromNonDepositor() public { uint128 _expiration = uint128(block.timestamp) + 1; uint256 _initialBalance = token.balanceOf(address(this)); address _other = 0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF; @@ -263,10 +273,10 @@ contract CollateralizationUnitTests is Test { vm.startPrank(_arbiter); collateralization.lock(_id); vm.warp(_expiration - 1); - collateralization.slash(_id); + collateralization.slash(_id, 1); vm.stopPrank(); assertEq(token.totalSupply(), _initialSupply - 1); - assertEq(uint256(collateralization.getDeposit(_id).state), uint256(DepositState.Slashed)); + assertEq(uint256(collateralization.getDeposit(_id).state), uint256(DepositState.Locked)); } function testFail_SlashAtExpiration() public { @@ -275,7 +285,7 @@ contract CollateralizationUnitTests is Test { uint128 _id = collateralization.deposit(1, _expiration, address(this)); collateralization.lock(_id); vm.warp(_expiration); - collateralization.slash(_id); + collateralization.slash(_id, 1); } function testFail_SlashAfterExpiration() public { @@ -284,7 +294,7 @@ contract CollateralizationUnitTests is Test { uint128 _id = collateralization.deposit(1, _expiration, address(this)); collateralization.lock(_id); vm.warp(_expiration + 1); - collateralization.slash(_id); + collateralization.slash(_id, 1); } function testFail_SlashUnlocked() public { @@ -292,7 +302,7 @@ contract CollateralizationUnitTests is Test { token.approve(address(collateralization), 1); uint128 _id = collateralization.deposit(1, _expiration, address(this)); vm.warp(_expiration + 1); - collateralization.slash(_id); + collateralization.slash(_id, 1); } function testFail_SlashFromNonArbiter() public { @@ -301,6 +311,6 @@ contract CollateralizationUnitTests is Test { token.approve(address(collateralization), 1); uint128 _id = collateralization.deposit(1, _expiration, _arbiter); vm.warp(_expiration - 1); - collateralization.slash(_id); + collateralization.slash(_id, 1); } } diff --git a/test/Example.t.sol b/test/Example.t.sol index 0d6b8fc..5e4c81a 100644 --- a/test/Example.t.sol +++ b/test/Example.t.sol @@ -7,7 +7,9 @@ import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; import {Collateralization, Deposit, DepositState, UnexpectedState} from "../src/Collateralization.sol"; import {DataService} from "../src/examples/DataService.sol"; import {Lender, Limits} from "../src/examples/Lender.sol"; -import {AggregatedLoan, IDataService, LoanAggregator, LoanCommitment} from "../src/examples/LoanAggregator.sol"; +import { + AggregatedLoan, IDataService, ILender, LoanAggregator, LoanCommitment +} from "../src/examples/LoanAggregator.sol"; contract TestToken is ERC20Burnable { constructor(uint256 _initialSupply) ERC20("MockCoin", "MOCK") { @@ -15,81 +17,86 @@ contract TestToken is ERC20Burnable { } } -contract CollateralizationUnitTests is Test { +contract CollateralizationUnitTests is Test, ILender { TestToken public token; Collateralization public collateralization; + DataService public dataService; + LoanAggregator public aggregator; + Lender public lender; function setUp() public { token = new TestToken(1_000); collateralization = new Collateralization(token); + aggregator = new LoanAggregator(collateralization); + dataService = new DataService(collateralization, 20 days); + lender = new Lender(aggregator, Limits({maxValue: 100, maxDuration: 30 days})); + token.transfer(address(lender), 80); } - function test_Example() public { - DataService _dataService = new DataService(collateralization, 20 days); - LoanAggregator _agg = new LoanAggregator(collateralization); - Lender _lender = new Lender(_agg, Limits({maxValue: 100, maxDuration: 30 days})); - token.transfer(address(_lender), 80); + function onCollateralWithraw(uint256 _value, uint96 _lenderData) public {} - token.approve(address(_dataService), 10); - _dataService.addProvider(address(this), 10); + function test_Example() public { + // Add this contract as a data service provider to receive 10 tokens in payment. + token.approve(address(dataService), 10); + dataService.addProvider(address(this), 10); uint256 _initialBalance = token.balanceOf(address(this)); - uint256 _initialLenderBalance = token.balanceOf(address(_lender)); + uint256 _initialLenderBalance = token.balanceOf(address(lender)); + // Data service requires 10x of payment (100 tokens) in collateralization deposit. Fund deposit with a 20 token + // loan from self, and a 80 token loan from lender. LoanCommitment[] memory _loanCommitments = new LoanCommitment[](2); - token.approve(address(_agg), 20); - _loanCommitments[0] = LoanCommitment({ - loan: AggregatedLoan({lender: address(this), value: 20, borrower: address(this), borrowerCollateral: 0}), - signature: "siggy" - }); - token.approve(address(_lender), 6); - _loanCommitments[1] = _lender.borrow(80, 5, 1, _dataService.disputePeriod()); - - uint128 _expiration = uint128(block.timestamp) + _dataService.disputePeriod(); - uint128 _deposit = _agg.remitPayment(DataService(_dataService), _expiration, _loanCommitments); + token.approve(address(aggregator), 20); + _loanCommitments[0] = + LoanCommitment({loan: AggregatedLoan({lender: this, lenderData: 0, value: 20}), signature: "siggy"}); + // Send lender 5 tokens in collateral and 1 token in payment (6 total) for a 80 token loan. + token.approve(address(lender), 6); + _loanCommitments[1] = lender.borrow(80, 5, 1, dataService.disputePeriod()); + // Receive 10 token payment and start dispute period. + uint128 _expiration = uint128(block.timestamp) + dataService.disputePeriod(); + uint128 _deposit = aggregator.remitPayment(DataService(dataService), _expiration, _loanCommitments); assertEq(token.balanceOf(address(this)), _initialBalance + 10 - 26); - assertEq(token.balanceOf(address(_lender)), _initialLenderBalance - 80 + 6); - - vm.warp(block.number + _dataService.disputePeriod()); - _agg.withdraw(_deposit); + assertEq(token.balanceOf(address(lender)), _initialLenderBalance + 6 - 80); + // Withdraw deposit at end of dispute period. + vm.warp(block.number + dataService.disputePeriod()); + aggregator.withdraw(_deposit); assertEq(token.balanceOf(address(this)), _initialBalance + 10 - 1); - assertEq(token.balanceOf(address(_lender)), _initialLenderBalance + 1); + assertEq(token.balanceOf(address(lender)), _initialLenderBalance + 1); } function test_ExampleSlash() public { - DataService _dataService = new DataService(collateralization, 20 days); - LoanAggregator _agg = new LoanAggregator(collateralization); - Lender _lender = new Lender(_agg, Limits({maxValue: 100, maxDuration: 30 days})); - token.transfer(address(_lender), 80); - - token.approve(address(_dataService), 10); - _dataService.addProvider(address(this), 10); + // Add this contract as a data service provider to receive 10 tokens in payment. + token.approve(address(dataService), 10); + dataService.addProvider(address(this), 10); uint256 _initialBalance = token.balanceOf(address(this)); - uint256 _initialLenderBalance = token.balanceOf(address(_lender)); + uint256 _initialLenderBalance = token.balanceOf(address(lender)); + // Data service requires 10x of payment (100 tokens) in collateralization deposit. Fund deposit with a 20 token + // loan from self, and a 80 token loan from lender. LoanCommitment[] memory _loanCommitments = new LoanCommitment[](2); - token.approve(address(_agg), 20); - _loanCommitments[0] = LoanCommitment({ - loan: AggregatedLoan({lender: address(this), value: 20, borrower: address(this), borrowerCollateral: 0}), - signature: "siggy" - }); - token.approve(address(_lender), 6); - _loanCommitments[1] = _lender.borrow(80, 5, 1, _dataService.disputePeriod()); - - uint128 _expiration = uint128(block.timestamp) + _dataService.disputePeriod(); - uint128 _deposit = _agg.remitPayment(DataService(_dataService), _expiration, _loanCommitments); + token.approve(address(aggregator), 20); + _loanCommitments[0] = + LoanCommitment({loan: AggregatedLoan({lender: this, lenderData: 0, value: 20}), signature: "siggy"}); + // Send lender 5 tokens in collateral and 1 token in payment (6 total) for a 80 token loan. + token.approve(address(lender), 6); + _loanCommitments[1] = lender.borrow(80, 5, 1, dataService.disputePeriod()); + // Receive 10 token payment and start dispute period. + uint128 _expiration = uint128(block.timestamp) + dataService.disputePeriod(); + uint128 _deposit = aggregator.remitPayment(DataService(dataService), _expiration, _loanCommitments); assertEq(token.balanceOf(address(this)), _initialBalance + 10 - 26); - assertEq(token.balanceOf(address(_lender)), _initialLenderBalance - 80 + 6); - - vm.warp(block.number + _dataService.disputePeriod() - 1); - _dataService.slash(address(this)); - - vm.warp(block.number + _dataService.disputePeriod()); - vm.expectRevert(abi.encodeWithSelector(UnexpectedState.selector, DepositState.Slashed)); - _agg.withdraw(_deposit); + assertEq(token.balanceOf(address(lender)), _initialLenderBalance + 6 - 80); + + // Warp to one block before disoute period end, and slash 80 tokens (80%) of deposit. + vm.warp(block.number + dataService.disputePeriod() - 1); + dataService.slash(address(this), 80); + // Warp to end of dispute period, and withdraw remaining tokens. + vm.warp(block.number + dataService.disputePeriod()); + aggregator.withdraw(_deposit); + assertEq(token.balanceOf(address(this)), _initialBalance + 10 - 22); + assertEq(token.balanceOf(address(lender)), _initialLenderBalance + 6 - 64); } } From f621d5dfd1001ab722de3dcd2391bbf64e236baf Mon Sep 17 00:00:00 2001 From: Theo Butler Date: Thu, 20 Jul 2023 09:37:45 -0400 Subject: [PATCH 09/23] Allow deposit ID reuse --- src/Collateralization.sol | 19 ++++++++---- src/examples/LoanAggregator.sol | 2 +- test/Collateralization.t.sol | 53 ++++++++++++++++++++------------- 3 files changed, 46 insertions(+), 28 deletions(-) diff --git a/src/Collateralization.sol b/src/Collateralization.sol index f8ee6ca..17f69e1 100644 --- a/src/Collateralization.sol +++ b/src/Collateralization.sol @@ -80,16 +80,23 @@ contract Collateralization { } /// Create a new deposit, returning its associated ID. + /// @param _id _id ID of the deposit ID to reuse. This should be set to zero to receive a new ID. IDs may only be + /// reused by its prior depositor when the deposit is withdrawn. /// @param _value Token value of the new deposit. /// @param _expiration Expiration timestamp of the new deposit, in seconds. /// @param _arbiter Arbiter of the new deposit. - /// @return id Unique ID associated with the new deposit. - function deposit(uint256 _value, uint128 _expiration, address _arbiter) public returns (uint128) { - // TODO: reuse state when given `(_id != 0) && (msg.sender == getDeposit(_id).depositor)` + /// @return id ID associated with the new deposit. + function deposit(uint128 _id, uint256 _value, uint128 _expiration, address _arbiter) public returns (uint128) { if (_value == 0) revert ZeroValue(); - if (block.timestamp >= _expiration) revert Expired(true); - lastID += 1; - uint128 _id = lastID; + if (_id == 0) { + if (block.timestamp >= _expiration) revert Expired(true); + lastID += 1; + _id = lastID; + } else { + Deposit memory _deposit = getDeposit(_id); + if (msg.sender != _deposit.depositor) revert NotDepositor(); + if (_deposit.state != DepositState.Withdrawn) revert UnexpectedState(_deposit.state); + } deposits[_id] = Deposit({ depositor: msg.sender, arbiter: _arbiter, diff --git a/src/examples/LoanAggregator.sol b/src/examples/LoanAggregator.sol index 4189c51..7d30d09 100644 --- a/src/examples/LoanAggregator.sol +++ b/src/examples/LoanAggregator.sol @@ -46,7 +46,7 @@ contract LoanAggregator { _index += 1; } collateralization.token().approve(address(collateralization), _value); - uint128 _deposit = collateralization.deposit(_value, _expiration, address(_arbiter)); + uint128 _deposit = collateralization.deposit(0, _value, _expiration, address(_arbiter)); _index = 0; while (_index < _loanCommitments.length) { loans[_deposit].push(_loanCommitments[_index].loan); diff --git a/test/Collateralization.t.sol b/test/Collateralization.t.sol index 1edc2f4..64ab433 100644 --- a/test/Collateralization.t.sol +++ b/test/Collateralization.t.sol @@ -6,7 +6,7 @@ import {StdUtils} from "forge-std/StdUtils.sol"; import {Test} from "forge-std/Test.sol"; import {ERC20Burnable} from "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.sol"; import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; -import {Collateralization, Deposit, DepositState} from "../src/Collateralization.sol"; +import {Collateralization, Deposit, DepositState, UnexpectedState} from "../src/Collateralization.sol"; contract TestToken is ERC20Burnable { constructor(uint256 _initialSupply) ERC20("MockCoin", "MOCK") { @@ -44,7 +44,7 @@ contract CollateralizationHandler is CommonBase, StdUtils { uint256 _value = bound(__value, 1, collateralization.token().balanceOf(_depositor)); vm.startPrank(_depositor); collateralization.token().approve(address(collateralization), _value); - uint128 _id = collateralization.deposit(_value, _genExpiration(__expiration), _genActor(__arbiter)); + uint128 _id = collateralization.deposit(0, _value, _genExpiration(__expiration), _genActor(__arbiter)); vm.stopPrank(); depositIDs.push(_id); return _id; @@ -137,7 +137,7 @@ contract CollateralizationUnitTests is Test { uint128 _expiration = uint128(block.timestamp) + 1; uint256 _initialBalance = token.balanceOf(address(this)); token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(1, _expiration, address(0)); + uint128 _id = collateralization.deposit(0, 1, _expiration, address(0)); assertEq(token.balanceOf(address(this)), _initialBalance - 1); assertEq(token.balanceOf(address(collateralization)), 1); assertEq(collateralization.getDeposit(_id).depositor, address(this)); @@ -148,27 +148,38 @@ contract CollateralizationUnitTests is Test { function test_DepositUniqueID() public { uint128 _expiration = uint128(block.timestamp) + 1; token.approve(address(collateralization), 2); - uint128 _id1 = collateralization.deposit(1, _expiration, address(0)); - uint128 _id2 = collateralization.deposit(1, _expiration, address(0)); + uint128 _id1 = collateralization.deposit(0, 1, _expiration, address(0)); + uint128 _id2 = collateralization.deposit(0, 1, _expiration, address(0)); assertNotEq(_id1, _id2); } + function test_DepositReusedID() public { + uint128 _expiration = uint128(block.timestamp) + 1; + token.approve(address(collateralization), 2); + uint128 _id1 = collateralization.deposit(0, 1, _expiration, address(0)); + vm.expectRevert(abi.encodeWithSelector(UnexpectedState.selector, DepositState.Unlocked)); + collateralization.deposit(_id1, 1, _expiration, address(0)); + collateralization.withdraw(_id1); + uint128 _id2 = collateralization.deposit(_id1, 1, _expiration, address(0)); + assertEq(_id1, _id2); + } + function testFail_DepositExpirationAtBlock() public { uint128 _expiration = uint128(block.timestamp); token.approve(address(collateralization), 1); - collateralization.deposit(1, _expiration, address(0)); + collateralization.deposit(0, 1, _expiration, address(0)); } function testFail_DepositExpirationBeforeBlock() public { uint128 _expiration = uint128(block.timestamp) - 1; token.approve(address(collateralization), 1); - collateralization.deposit(1, _expiration, address(0)); + collateralization.deposit(0, 1, _expiration, address(0)); } function test_Lock() public { uint128 _expiration = uint128(block.timestamp) + 1; token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(1, _expiration, address(this)); + uint128 _id = collateralization.deposit(0, 1, _expiration, address(this)); assertEq(uint256(collateralization.getDeposit(_id).state), uint256(DepositState.Unlocked)); collateralization.lock(_id); assertEq(uint256(collateralization.getDeposit(_id).state), uint256(DepositState.Locked)); @@ -178,7 +189,7 @@ contract CollateralizationUnitTests is Test { function testFail_LockAtExpiration() public { uint128 _expiration = uint128(block.timestamp) + 1; token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(1, _expiration, address(this)); + uint128 _id = collateralization.deposit(0, 1, _expiration, address(this)); vm.warp(_expiration); collateralization.lock(_id); } @@ -186,7 +197,7 @@ contract CollateralizationUnitTests is Test { function testFail_LockAfterExpiration() public { uint128 _expiration = uint128(block.timestamp) + 1; token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(1, _expiration, address(this)); + uint128 _id = collateralization.deposit(0, 1, _expiration, address(this)); vm.warp(_expiration + 1); collateralization.lock(_id); } @@ -199,7 +210,7 @@ contract CollateralizationUnitTests is Test { uint128 _expiration = uint128(block.timestamp) + 1; uint256 _initialBalance = token.balanceOf(address(this)); token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(1, _expiration, address(this)); + uint128 _id = collateralization.deposit(0, 1, _expiration, address(this)); collateralization.lock(_id); vm.warp(_expiration); collateralization.withdraw(_id); @@ -212,7 +223,7 @@ contract CollateralizationUnitTests is Test { uint128 _expiration = uint128(block.timestamp) + 1; uint256 _initialBalance = token.balanceOf(address(this)); token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(1, _expiration, address(this)); + uint128 _id = collateralization.deposit(0, 1, _expiration, address(this)); collateralization.lock(_id); vm.warp(_expiration + 1); collateralization.withdraw(_id); @@ -223,7 +234,7 @@ contract CollateralizationUnitTests is Test { function testFail_WithdrawBeforeExpiration() public { token.approve(address(collateralization), 1); uint128 _expiration = uint128(block.timestamp) + 3; - uint128 _id = collateralization.deposit(1, _expiration, address(this)); + uint128 _id = collateralization.deposit(0, 1, _expiration, address(this)); collateralization.lock(_id); vm.warp(_expiration - 1); collateralization.withdraw(_id); @@ -233,7 +244,7 @@ contract CollateralizationUnitTests is Test { uint128 _expiration = uint128(block.timestamp) + 1; uint256 _initialBalance = token.balanceOf(address(this)); token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(1, _expiration, address(this)); + uint128 _id = collateralization.deposit(0, 1, _expiration, address(this)); collateralization.lock(_id); vm.warp(_expiration); collateralization.withdraw(_id); @@ -244,7 +255,7 @@ contract CollateralizationUnitTests is Test { function testFail_WithdrawTwice() public { uint128 _expiration = uint128(block.timestamp) + 1; token.approve(address(collateralization), 2); - uint128 _id = collateralization.deposit(2, _expiration, address(this)); + uint128 _id = collateralization.deposit(0, 2, _expiration, address(this)); collateralization.lock(_id); vm.warp(_expiration); collateralization.withdraw(_id); @@ -256,7 +267,7 @@ contract CollateralizationUnitTests is Test { uint256 _initialBalance = token.balanceOf(address(this)); address _other = 0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF; token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(1, _expiration, address(this)); + uint128 _id = collateralization.deposit(0, 1, _expiration, address(this)); collateralization.lock(_id); vm.warp(_expiration); vm.prank(_other); @@ -269,7 +280,7 @@ contract CollateralizationUnitTests is Test { address _arbiter = 0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF; uint256 _initialSupply = token.totalSupply(); token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(1, _expiration, _arbiter); + uint128 _id = collateralization.deposit(0, 1, _expiration, _arbiter); vm.startPrank(_arbiter); collateralization.lock(_id); vm.warp(_expiration - 1); @@ -282,7 +293,7 @@ contract CollateralizationUnitTests is Test { function testFail_SlashAtExpiration() public { uint128 _expiration = uint128(block.timestamp) + 3; token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(1, _expiration, address(this)); + uint128 _id = collateralization.deposit(0, 1, _expiration, address(this)); collateralization.lock(_id); vm.warp(_expiration); collateralization.slash(_id, 1); @@ -291,7 +302,7 @@ contract CollateralizationUnitTests is Test { function testFail_SlashAfterExpiration() public { uint128 _expiration = uint128(block.timestamp) + 3; token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(1, _expiration, address(this)); + uint128 _id = collateralization.deposit(0, 1, _expiration, address(this)); collateralization.lock(_id); vm.warp(_expiration + 1); collateralization.slash(_id, 1); @@ -300,7 +311,7 @@ contract CollateralizationUnitTests is Test { function testFail_SlashUnlocked() public { uint128 _expiration = uint128(block.timestamp) + 3; token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(1, _expiration, address(this)); + uint128 _id = collateralization.deposit(0, 1, _expiration, address(this)); vm.warp(_expiration + 1); collateralization.slash(_id, 1); } @@ -309,7 +320,7 @@ contract CollateralizationUnitTests is Test { uint128 _expiration = uint128(block.timestamp) + 3; address _arbiter = 0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF; token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(1, _expiration, _arbiter); + uint128 _id = collateralization.deposit(0, 1, _expiration, _arbiter); vm.warp(_expiration - 1); collateralization.slash(_id, 1); } From 5994d5150162f741282568d59868de9f61d35c0a Mon Sep 17 00:00:00 2001 From: Theo Butler Date: Sun, 23 Jul 2023 15:17:57 -0400 Subject: [PATCH 10/23] Support unlocked deposits --- src/Collateralization.sol | 209 +++++++++++++--------------- src/examples/DataService.sol | 14 +- src/examples/Lender.sol | 4 +- src/examples/LoanAggregator.sol | 14 +- test/Collateralization.t.sol | 235 ++++++++++++++------------------ test/Example.t.sol | 14 +- 6 files changed, 219 insertions(+), 271 deletions(-) diff --git a/src/Collateralization.sol b/src/Collateralization.sol index 17f69e1..473bc68 100644 --- a/src/Collateralization.sol +++ b/src/Collateralization.sol @@ -3,73 +3,59 @@ pragma solidity ^0.8.13; import "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.sol"; -/// A Deposit describes a slashable, time-locked deposit of `value` tokens. A Deposit must be in the locked `state` to -/// provide the following invariants: -/// - The `arbiter` has the authority to slash the Deposit before `expiration`, which burns a given amount tokens. A -/// slash also reduces the tokens eventually available to withdraw by the same amount. -/// - A Deposit may only be withdrawn when `block.timestamp >= expiration`. Withdrawal returns `value` tokens to the -/// depositor. -struct Deposit { +/// The state associated with a slashable, potentially time-locked deposit of tokens. When a deposit is locked +/// (`block.timestamp < unlock`) it has the following properties: +/// - A deposit may only be withdrawn when the deposit is unlocked (`block.timestamp >= unlock`). Withdrawal returns the +/// deposit's token value to the depositor. +/// - The arbiter has authority to slash the deposit before unlock, which burns a given amount tokens. A slash also +/// reduces the tokens available to withdraw by the same amount. +struct DepositState { + // creator of the deposit, has ability to withdraw when the deposit is unlocked address depositor; + // authority to slash deposit value, when the deposit is locked address arbiter; + // token amount associated with deposit uint256 value; - uint128 expiration; - DepositState state; + // timestamp when deposit is no longer locked + uint64 unlock; + // timestamp of deposit creation + uint64 start; + // timestamp of withdrawal, 0 until withdrawn + uint64 end; } -/// ┌────────┐ ┌──────┐ ┌─────────┐ -/// │Unlocked│ │Locked│ │Withdrawn│ -/// └───┬────┘ └──┬───┘ └────┬────┘ -/// │ lock │ │ -/// │ ─────────────────> │ -/// │ │ │ -/// │ │────┐ │ -/// │ │ │ slash │ -/// │ │<───┘ │ -/// │ │ │ -/// │ │ withdraw │ -/// │ │ ─────────────────>│ -/// │ │ │ -/// │ withdraw │ -/// │ ────────────────────────────────────>│ -/// │ │ │ -/// │ deposit │ -/// │ <────────────────────────────────────│ -/// ┌───┴────┐ ┌──┴───┐ ┌────┴────┐ -/// │Unlocked│ │Locked│ │Withdrawn│ -/// └────────┘ └──────┘ └─────────┘ -enum DepositState { - Unlocked, - Locked, - Withdrawn -} - -/// Deposit in unexpected state. -error UnexpectedState(DepositState state); -/// Deposit value is zero. -error ZeroValue(); -/// Deposit expiration in unexpected state. -error Expired(bool expired); -/// Withdraw called by an address that isn't the depositor. -error NotDepositor(); -/// Slash called by an address that isn't the deposit's arbiter. -error NotArbiter(); -/// Deposit does not exist. -error NotFound(); -/// Slash amount is larger than remainning deposit balance. -error SlashAmountTooLarge(); +// ┌────────┐ ┌──────┐ ┌─────────┐ +// │unlocked│ │locked│ │withdrawn│ +// └───┬────┘ └──┬───┘ └────┬────┘ +// deposit (unlock == 0) │ │ │ +// ─────────────────────>│ │ │ +// │ │ │ +// deposit (unlock != 0) │ │ +// ───────────────────────────────────────────────────────>│ │ +// │ │ │ +// │ lock (block.timestamp < _unlock)│ │ +// │ ───────────────────────────────>│ │ +// │ │ │ +// │ (block.timestamp >= unlock) │ │ +// │ <─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│ │ +// │ │ │ +// │ withdraw │ │ +// │ ───────────────────────────────────────────────────>│ +// ┌───┴────┐ ┌──┴───┐ ┌────┴────┐ +// │unlocked│ │locked│ │withdrawn│ +// └────────┘ └──────┘ └─────────┘ /// This contract manages Deposits as described above. contract Collateralization { - event _Deposit(uint128 indexed id, address indexed arbiter, uint256 value, uint128 expiration); - event _Lock(uint128 indexed id); - event _Withdraw(uint128 indexed id); - event _Slash(uint128 indexed id, uint256 amount); + event Deposit(uint128 indexed id, address indexed arbiter, uint256 value, uint64 unlock); + event Lock(uint128 indexed id, uint64 unlock); + event Slash(uint128 indexed id, uint256 amount); + event Withdraw(uint128 indexed id); /// Burnable ERC-20 token held by this contract. ERC20Burnable public token; /// Mapping of deposit IDs to deposits. - mapping(uint128 => Deposit) public deposits; + mapping(uint128 => DepositState) public deposits; /// Counter for assigning new deposit IDs. uint128 public lastID; @@ -80,94 +66,81 @@ contract Collateralization { } /// Create a new deposit, returning its associated ID. - /// @param _id _id ID of the deposit ID to reuse. This should be set to zero to receive a new ID. IDs may only be - /// reused by its prior depositor when the deposit is withdrawn. - /// @param _value Token value of the new deposit. - /// @param _expiration Expiration timestamp of the new deposit, in seconds. /// @param _arbiter Arbiter of the new deposit. - /// @return id ID associated with the new deposit. - function deposit(uint128 _id, uint256 _value, uint128 _expiration, address _arbiter) public returns (uint128) { - if (_value == 0) revert ZeroValue(); - if (_id == 0) { - if (block.timestamp >= _expiration) revert Expired(true); - lastID += 1; - _id = lastID; - } else { - Deposit memory _deposit = getDeposit(_id); - if (msg.sender != _deposit.depositor) revert NotDepositor(); - if (_deposit.state != DepositState.Withdrawn) revert UnexpectedState(_deposit.state); - } - deposits[_id] = Deposit({ + /// @param _value Initial token value of the new deposit. + /// @param _unlock Unlock timestamp of the new deposit, in seconds. Set to a nonzero value to lock deposit. + /// @return id Unique ID associated with the new deposit. + function deposit(address _arbiter, uint256 _value, uint64 _unlock) public returns (uint128) { + lastID += 1; + deposits[lastID] = DepositState({ depositor: msg.sender, arbiter: _arbiter, value: _value, - expiration: _expiration, - state: DepositState.Unlocked + unlock: _unlock, + start: uint64(block.timestamp), + end: 0 }); bool _transferSuccess = token.transferFrom(msg.sender, address(this), _value); require(_transferSuccess, "transfer failed"); - emit _Deposit(_id, _arbiter, _value, _expiration); - return _id; - } - - /// Lock the deposit associated with the given ID. This makes the deposit slashable until the deposit - /// expiration. - /// @param _id ID of the associated deposit. - function lock(uint128 _id) public { - Deposit memory _deposit = getDeposit(_id); - if (msg.sender != _deposit.arbiter) revert NotArbiter(); - if (_deposit.state != DepositState.Unlocked) revert UnexpectedState(_deposit.state); - if (block.timestamp >= _deposit.expiration) revert Expired(true); - deposits[_id].state = DepositState.Locked; - emit _Lock(_id); + emit Deposit(lastID, _arbiter, _value, _unlock); + return lastID; } - /// Unlock the deposit associated with the given ID and return its associated tokens to the depositor. + /// Lock the deposit associated with the given ID. This makes the deposit slashable until it is unlocked. /// @param _id ID of the associated deposit. - function withdraw(uint128 _id) public { - Deposit memory _deposit = getDeposit(_id); - if (_deposit.depositor != msg.sender) revert NotDepositor(); - DepositState _state = deposits[_id].state; - if (_state == DepositState.Locked) { - if (block.timestamp < _deposit.expiration) revert Expired(false); - } else if (_state != DepositState.Unlocked) { - revert UnexpectedState(_state); + /// @param _unlock Unlock timestamp of deposit, in seconds. + function lock(uint128 _id, uint64 _unlock) public { + DepositState memory _deposit = getDeposit(_id); + require(msg.sender == _deposit.arbiter, "sender not arbiter"); + require(_deposit.end == 0, "deposit withdrawn"); + if (_deposit.unlock == _unlock) { + return; } - deposits[_id].state = DepositState.Withdrawn; - bool _transferSuccess = token.transfer(_deposit.depositor, _deposit.value); - require(_transferSuccess, "transfer failed"); - emit _Withdraw(_id); + require(_deposit.unlock == 0, "deposit locked"); + deposits[_id].unlock = _unlock; + emit Lock(_id, _unlock); } - /// Burn some amount of the deposit value prior to expiration. This action can only be performed by the arbiter of + /// Burn some amount of the deposit value while it's locked. This action can only be performed by the arbiter of /// the deposit associated with the given ID. /// @param _id ID of the associated deposit. - /// @param _amount Amount of remaining tokens to burn. + /// @param _amount Amount of remaining deposit tokens to burn. function slash(uint128 _id, uint256 _amount) public { - Deposit memory _deposit = getDeposit(_id); - if (msg.sender != _deposit.arbiter) revert NotArbiter(); - if (_deposit.state != DepositState.Locked) revert UnexpectedState(_deposit.state); - if (block.timestamp >= _deposit.expiration) revert Expired(true); - if (_amount > _deposit.value) revert SlashAmountTooLarge(); + DepositState memory _deposit = getDeposit(_id); + require(msg.sender == _deposit.arbiter, "sender not arbiter"); + require(_deposit.end == 0, "deposit withdrawn"); + require(block.timestamp < _deposit.unlock, "deposit unlocked"); + require(_amount <= _deposit.value, "amount too large"); deposits[_id].value -= _amount; token.burn(_amount); - emit _Slash(_id, _amount); + emit Slash(_id, _amount); + } + + /// Collect remaining tokens associated with a deposit. + /// @param _id ID of the associated deposit. + function withdraw(uint128 _id) public { + DepositState memory _deposit = getDeposit(_id); + require(_deposit.depositor == msg.sender, "sender not depositor"); + require(_deposit.end == 0, "deposit withdrawn"); + require(block.timestamp >= _deposit.unlock, "deposit locked"); + deposits[_id].end = uint64(block.timestamp); + bool _transferSuccess = token.transfer(_deposit.depositor, _deposit.value); + require(_transferSuccess, "transfer failed"); + emit Withdraw(_id); } - /// Return the deposit associated with the given ID. + /// Return the deposit state associated with the given ID. /// @param _id ID of the associated deposit. - function getDeposit(uint128 _id) public view returns (Deposit memory) { - Deposit memory _deposit = deposits[_id]; - if (_deposit.depositor == address(0)) revert NotFound(); + function getDeposit(uint128 _id) public view returns (DepositState memory) { + DepositState memory _deposit = deposits[_id]; + require(_deposit.depositor != address(0), "deposit not found"); return _deposit; } - /// Return true if the deposit associated with the given ID is slashable, false otherwise. A slashable deposit is - /// locked and not expired. + /// Return true if the deposit associated with the given ID is slashable, false otherwise. /// @param _id ID of the associated deposit. function isSlashable(uint128 _id) public view returns (bool) { - Deposit memory _deposit = getDeposit(_id); - // TODO: also check if `_deposit.value > 0`? - return (_deposit.state == DepositState.Locked) && (block.timestamp < _deposit.expiration); + DepositState memory _deposit = getDeposit(_id); + return (block.timestamp < _deposit.unlock); } } diff --git a/src/examples/DataService.sol b/src/examples/DataService.sol index 1d036c9..a330c4a 100644 --- a/src/examples/DataService.sol +++ b/src/examples/DataService.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.13; import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol"; -import {Collateralization, Deposit} from "../Collateralization.sol"; +import {Collateralization, DepositState} from "../Collateralization.sol"; import {IDataService} from "./LoanAggregator.sol"; contract DataService is Ownable, IDataService { @@ -13,9 +13,9 @@ contract DataService is Ownable, IDataService { Collateralization public collateralization; mapping(address => ProviderState) public providers; - uint128 public disputePeriod; + uint64 public disputePeriod; - constructor(Collateralization _collateralization, uint128 _disputePeriod) { + constructor(Collateralization _collateralization, uint64 _disputePeriod) { collateralization = _collateralization; disputePeriod = _disputePeriod; } @@ -41,17 +41,17 @@ contract DataService is Ownable, IDataService { } /// Called by data service provider to receive payment. This locks the given deposit to begin a dispute period. - function remitPayment(address _providerAddr, uint128 _depositID) public { + function remitPayment(address _providerAddr, uint128 _depositID, uint64 _unlock) public { ProviderState memory _provider = getProviderState(_providerAddr); - Deposit memory _deposit = collateralization.getDeposit(_depositID); + DepositState memory _deposit = collateralization.getDeposit(_depositID); uint256 minCollateral = uint256(_provider.payment) * 10; require(_deposit.value >= minCollateral, "collateral below minimum"); uint128 disputePeriodEnd = uint128(block.timestamp + disputePeriod); - require(_deposit.expiration >= disputePeriodEnd, "collateral expiration before end of dispute period"); + require(_unlock >= disputePeriodEnd, "collateral unlock before end of dispute period"); providers[_providerAddr].deposit = _depositID; - collateralization.lock(_depositID); + collateralization.lock(_depositID, _unlock); collateralization.token().transfer(_providerAddr, _provider.payment); } diff --git a/src/examples/Lender.sol b/src/examples/Lender.sol index dece8f0..ee78e6c 100644 --- a/src/examples/Lender.sol +++ b/src/examples/Lender.sol @@ -30,12 +30,12 @@ contract Lender is Ownable, ILender { return agg.collateralization().token().transfer(owner(), _amount); } - function borrow(uint256 _value, uint256 _collateral, uint256 _payment, uint128 _expiration) + function borrow(uint256 _value, uint256 _collateral, uint256 _payment, uint64 _unlock) public returns (LoanCommitment memory) { require(_collateral <= _value, "collateral > value"); - uint64 _duration = SafeCast.toUint64(_expiration - block.timestamp); + uint64 _duration = SafeCast.toUint64(_unlock - block.timestamp); require(_duration <= limits.maxDuration, "duration over maximum"); require(_value <= limits.maxValue, "value over maximum"); require(_payment >= expectedPayment(_value, _duration), "payment below expected"); diff --git a/src/examples/LoanAggregator.sol b/src/examples/LoanAggregator.sol index 7d30d09..1820177 100644 --- a/src/examples/LoanAggregator.sol +++ b/src/examples/LoanAggregator.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.13; -import {Collateralization, Deposit, DepositState} from "../Collateralization.sol"; +import {Collateralization, DepositState} from "../Collateralization.sol"; interface IDataService { - function remitPayment(address _provider, uint128 _deposit) external; + function remitPayment(address _provider, uint128 _deposit, uint64 _unlock) external; } interface ILender { @@ -30,7 +30,7 @@ contract LoanAggregator { collateralization = _collateralization; } - function remitPayment(IDataService _arbiter, uint128 _expiration, LoanCommitment[] calldata _loanCommitments) + function remitPayment(IDataService _arbiter, uint64 _unlock, LoanCommitment[] calldata _loanCommitments) public returns (uint128) { @@ -38,7 +38,7 @@ contract LoanAggregator { uint256 _value = 0; while (_index < _loanCommitments.length) { LoanCommitment memory _commitment = _loanCommitments[_index]; - // TODO: verify signature of (lender, value, arbiter, expiration) + // TODO: verify signature of (lender, value, arbiter, unlock) _value += _commitment.loan.value; collateralization.token().transferFrom( address(_commitment.loan.lender), address(this), _commitment.loan.value @@ -46,18 +46,18 @@ contract LoanAggregator { _index += 1; } collateralization.token().approve(address(collateralization), _value); - uint128 _deposit = collateralization.deposit(0, _value, _expiration, address(_arbiter)); + uint128 _deposit = collateralization.deposit(address(_arbiter), _value, _unlock); _index = 0; while (_index < _loanCommitments.length) { loans[_deposit].push(_loanCommitments[_index].loan); _index += 1; } - _arbiter.remitPayment(msg.sender, _deposit); + _arbiter.remitPayment(msg.sender, _deposit, _unlock); return _deposit; } function withdraw(uint128 _depositID) public { - Deposit memory _deposit = collateralization.getDeposit(_depositID); + DepositState memory _deposit = collateralization.getDeposit(_depositID); collateralization.withdraw(_depositID); // calculate original deposit value uint256 _index = 0; diff --git a/test/Collateralization.t.sol b/test/Collateralization.t.sol index 64ab433..348d331 100644 --- a/test/Collateralization.t.sol +++ b/test/Collateralization.t.sol @@ -6,7 +6,7 @@ import {StdUtils} from "forge-std/StdUtils.sol"; import {Test} from "forge-std/Test.sol"; import {ERC20Burnable} from "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.sol"; import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; -import {Collateralization, Deposit, DepositState, UnexpectedState} from "../src/Collateralization.sol"; +import {Collateralization, DepositState} from "../src/Collateralization.sol"; contract TestToken is ERC20Burnable { constructor(uint256 _initialSupply) ERC20("MockCoin", "MOCK") { @@ -36,23 +36,20 @@ contract CollateralizationHandler is CommonBase, StdUtils { vm.warp(block.timestamp + bound(blocks, 1, 10)); } - function deposit(uint256 __sender, uint256 __value, uint256 __expiration, uint256 __arbiter) - public - returns (uint128) - { + function deposit(uint256 __sender, uint256 __arbiter, uint256 __value, uint256 __unlock) public returns (uint128) { address _depositor = _genActor(__sender); uint256 _value = bound(__value, 1, collateralization.token().balanceOf(_depositor)); vm.startPrank(_depositor); collateralization.token().approve(address(collateralization), _value); - uint128 _id = collateralization.deposit(0, _value, _genExpiration(__expiration), _genActor(__arbiter)); + uint128 _id = collateralization.deposit(_genActor(__arbiter), _value, _genTimestamp(__unlock)); vm.stopPrank(); depositIDs.push(_id); return _id; } - function lock(uint256 __sender, uint256 __id) public { + function lock(uint256 __sender, uint256 __id, uint256 __unlock) public { vm.prank(_genActor(__sender)); - collateralization.lock(_genID(__id)); + collateralization.lock(_genID(__id), _genTimestamp(__unlock)); } function withdraw(uint256 __sender, uint256 __id) public { @@ -75,7 +72,7 @@ contract CollateralizationHandler is CommonBase, StdUtils { uint64 _index = 0; while (_index < depositIDs.length) { uint128 _id = depositIDs[_index]; - Deposit memory _deposit = collateralization.getDeposit(_id); + DepositState memory _deposit = collateralization.getDeposit(_id); if (_deposit.depositor != address(0)) { total += _deposit.value; } @@ -104,8 +101,8 @@ contract CollateralizationHandler is CommonBase, StdUtils { return actors[bound(_seed, 0, actors.length - 1)]; } - function _genExpiration(uint256 _seed) internal view returns (uint128) { - return uint128(bound(_seed, 1, 20)); + function _genTimestamp(uint256 _seed) internal view returns (uint64) { + return uint64(bound(_seed, 1, 20)); } } @@ -133,195 +130,173 @@ contract CollateralizationUnitTests is Test { collateralization = new Collateralization(token); } - function test_Deposit() public { - uint128 _expiration = uint128(block.timestamp) + 1; + function test_UnlockedDeposit() public { uint256 _initialBalance = token.balanceOf(address(this)); token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(0, 1, _expiration, address(0)); + uint128 _id = collateralization.deposit(address(0), 1, 0); assertEq(token.balanceOf(address(this)), _initialBalance - 1); assertEq(token.balanceOf(address(collateralization)), 1); assertEq(collateralization.getDeposit(_id).depositor, address(this)); - assertEq(uint256(collateralization.getDeposit(_id).state), uint256(DepositState.Unlocked)); assertEq(collateralization.isSlashable(_id), false); } + function test_LockedDeposit() public { + uint64 _unlock = uint64(block.timestamp) + 1; + uint256 _initialBalance = token.balanceOf(address(this)); + token.approve(address(collateralization), 1); + uint128 _id = collateralization.deposit(address(0), 1, _unlock); + assertEq(token.balanceOf(address(this)), _initialBalance - 1); + assertEq(token.balanceOf(address(collateralization)), 1); + assertEq(collateralization.getDeposit(_id).depositor, address(this)); + assertEq(collateralization.isSlashable(_id), true); + } + function test_DepositUniqueID() public { - uint128 _expiration = uint128(block.timestamp) + 1; token.approve(address(collateralization), 2); - uint128 _id1 = collateralization.deposit(0, 1, _expiration, address(0)); - uint128 _id2 = collateralization.deposit(0, 1, _expiration, address(0)); + uint128 _id1 = collateralization.deposit(address(0), 1, 0); + uint128 _id2 = collateralization.deposit(address(0), 1, 0); assertNotEq(_id1, _id2); } - function test_DepositReusedID() public { - uint128 _expiration = uint128(block.timestamp) + 1; - token.approve(address(collateralization), 2); - uint128 _id1 = collateralization.deposit(0, 1, _expiration, address(0)); - vm.expectRevert(abi.encodeWithSelector(UnexpectedState.selector, DepositState.Unlocked)); - collateralization.deposit(_id1, 1, _expiration, address(0)); - collateralization.withdraw(_id1); - uint128 _id2 = collateralization.deposit(_id1, 1, _expiration, address(0)); - assertEq(_id1, _id2); + function test_Lock() public { + uint64 _unlock = uint64(block.timestamp) + 1; + token.approve(address(collateralization), 1); + uint128 _id = collateralization.deposit(address(this), 1, 0); + assertEq(collateralization.isSlashable(_id), false); + collateralization.lock(_id, _unlock); + assertEq(collateralization.isSlashable(_id), true); } - function testFail_DepositExpirationAtBlock() public { - uint128 _expiration = uint128(block.timestamp); + function test_LockLocked() public { + uint64 _unlock = uint64(block.timestamp) + 1; token.approve(address(collateralization), 1); - collateralization.deposit(0, 1, _expiration, address(0)); + uint128 _id = collateralization.deposit(address(this), 1, _unlock); + collateralization.lock(_id, _unlock); } - function testFail_DepositExpirationBeforeBlock() public { - uint128 _expiration = uint128(block.timestamp) - 1; + function testFail_LockLockedModify() public { + uint64 _unlock = uint64(block.timestamp) + 1; token.approve(address(collateralization), 1); - collateralization.deposit(0, 1, _expiration, address(0)); + uint128 _id = collateralization.deposit(address(this), 1, _unlock); + collateralization.lock(_id, _unlock - 1); } - function test_Lock() public { - uint128 _expiration = uint128(block.timestamp) + 1; + function testFail_LockAfterUnlock() public { + uint64 _unlock = uint64(block.timestamp) + 1; token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(0, 1, _expiration, address(this)); - assertEq(uint256(collateralization.getDeposit(_id).state), uint256(DepositState.Unlocked)); - collateralization.lock(_id); - assertEq(uint256(collateralization.getDeposit(_id).state), uint256(DepositState.Locked)); - assertEq(collateralization.isSlashable(_id), true); + uint128 _id = collateralization.deposit(address(this), 1, _unlock); + vm.warp(_unlock + 1); + collateralization.lock(_id, _unlock + 1); + } + + function testFail_getDepositNoDeposit() public view { + collateralization.getDeposit(0); } - function testFail_LockAtExpiration() public { - uint128 _expiration = uint128(block.timestamp) + 1; + function test_Slash() public { + uint64 _unlock = uint64(block.timestamp) + 3; + address _arbiter = 0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF; + uint256 _initialSupply = token.totalSupply(); token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(0, 1, _expiration, address(this)); - vm.warp(_expiration); - collateralization.lock(_id); + uint128 _id = collateralization.deposit(_arbiter, 1, _unlock); + vm.warp(_unlock - 1); + vm.prank(_arbiter); + collateralization.slash(_id, 1); + assertEq(token.totalSupply(), _initialSupply - 1); } - function testFail_LockAfterExpiration() public { - uint128 _expiration = uint128(block.timestamp) + 1; + function testFail_SlashAtUnlock() public { + uint64 _unlock = uint64(block.timestamp) + 3; token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(0, 1, _expiration, address(this)); - vm.warp(_expiration + 1); - collateralization.lock(_id); + uint128 _id = collateralization.deposit(address(this), 1, _unlock); + vm.warp(_unlock); + collateralization.slash(_id, 1); } - function testFail_getDepositNoDeposit() public view { - collateralization.getDeposit(0); + function testFail_SlashAfterUnlock() public { + uint64 _unlock = uint64(block.timestamp) + 3; + token.approve(address(collateralization), 1); + uint128 _id = collateralization.deposit(address(this), 1, _unlock); + vm.warp(_unlock + 1); + collateralization.slash(_id, 1); + } + + function testFail_SlashUnlocked() public { + uint64 _unlock = uint64(block.timestamp) + 3; + token.approve(address(collateralization), 1); + uint128 _id = collateralization.deposit(address(this), 1, _unlock); + vm.warp(_unlock + 1); + collateralization.slash(_id, 1); } - function test_WithdrawAtExpiration() public { - uint128 _expiration = uint128(block.timestamp) + 1; + function testFail_SlashFromNonArbiter() public { + uint64 _unlock = uint64(block.timestamp) + 3; + address _arbiter = 0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF; + token.approve(address(collateralization), 1); + uint128 _id = collateralization.deposit(_arbiter, 1, _unlock); + vm.warp(_unlock - 1); + collateralization.slash(_id, 1); + } + + function test_WithdrawAtUnlock() public { + uint64 _unlock = uint64(block.timestamp) + 1; uint256 _initialBalance = token.balanceOf(address(this)); token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(0, 1, _expiration, address(this)); - collateralization.lock(_id); - vm.warp(_expiration); + uint128 _id = collateralization.deposit(address(this), 1, _unlock); + vm.warp(_unlock); collateralization.withdraw(_id); assertEq(token.balanceOf(address(this)), _initialBalance); assertEq(token.balanceOf(address(collateralization)), 0); - assertEq(uint256(collateralization.getDeposit(_id).state), uint256(DepositState.Withdrawn)); } - function test_WithdrawAfterExpiration() public { - uint128 _expiration = uint128(block.timestamp) + 1; + function test_WithdrawAfterUnlock() public { + uint64 _unlock = uint64(block.timestamp) + 1; uint256 _initialBalance = token.balanceOf(address(this)); token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(0, 1, _expiration, address(this)); - collateralization.lock(_id); - vm.warp(_expiration + 1); + uint128 _id = collateralization.deposit(address(this), 1, _unlock); + vm.warp(_unlock + 1); collateralization.withdraw(_id); assertEq(token.balanceOf(address(this)), _initialBalance); assertEq(token.balanceOf(address(collateralization)), 0); } - function testFail_WithdrawBeforeExpiration() public { + function testFail_WithdrawBeforeUnlock() public { token.approve(address(collateralization), 1); - uint128 _expiration = uint128(block.timestamp) + 3; - uint128 _id = collateralization.deposit(0, 1, _expiration, address(this)); - collateralization.lock(_id); - vm.warp(_expiration - 1); + uint64 _unlock = uint64(block.timestamp) + 3; + uint128 _id = collateralization.deposit(address(this), 1, _unlock); + vm.warp(_unlock - 1); collateralization.withdraw(_id); } function test_WithdrawLocked() public { - uint128 _expiration = uint128(block.timestamp) + 1; + uint64 _unlock = uint64(block.timestamp) + 1; uint256 _initialBalance = token.balanceOf(address(this)); token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(0, 1, _expiration, address(this)); - collateralization.lock(_id); - vm.warp(_expiration); + uint128 _id = collateralization.deposit(address(this), 1, _unlock); + vm.warp(_unlock); collateralization.withdraw(_id); assertEq(token.balanceOf(address(this)), _initialBalance); assertEq(token.balanceOf(address(collateralization)), 0); } function testFail_WithdrawTwice() public { - uint128 _expiration = uint128(block.timestamp) + 1; + uint64 _unlock = uint64(block.timestamp) + 1; token.approve(address(collateralization), 2); - uint128 _id = collateralization.deposit(0, 2, _expiration, address(this)); - collateralization.lock(_id); - vm.warp(_expiration); + uint128 _id = collateralization.deposit(address(this), 2, _unlock); + vm.warp(_unlock); collateralization.withdraw(_id); collateralization.withdraw(_id); } function testFail_WithdrawFromNonDepositor() public { - uint128 _expiration = uint128(block.timestamp) + 1; + uint64 _unlock = uint64(block.timestamp) + 1; uint256 _initialBalance = token.balanceOf(address(this)); address _other = 0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF; token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(0, 1, _expiration, address(this)); - collateralization.lock(_id); - vm.warp(_expiration); + uint128 _id = collateralization.deposit(address(this), 1, _unlock); + vm.warp(_unlock); vm.prank(_other); collateralization.withdraw(_id); assertEq(token.balanceOf(address(this)), _initialBalance); } - - function test_Slash() public { - uint128 _expiration = uint128(block.timestamp) + 3; - address _arbiter = 0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF; - uint256 _initialSupply = token.totalSupply(); - token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(0, 1, _expiration, _arbiter); - vm.startPrank(_arbiter); - collateralization.lock(_id); - vm.warp(_expiration - 1); - collateralization.slash(_id, 1); - vm.stopPrank(); - assertEq(token.totalSupply(), _initialSupply - 1); - assertEq(uint256(collateralization.getDeposit(_id).state), uint256(DepositState.Locked)); - } - - function testFail_SlashAtExpiration() public { - uint128 _expiration = uint128(block.timestamp) + 3; - token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(0, 1, _expiration, address(this)); - collateralization.lock(_id); - vm.warp(_expiration); - collateralization.slash(_id, 1); - } - - function testFail_SlashAfterExpiration() public { - uint128 _expiration = uint128(block.timestamp) + 3; - token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(0, 1, _expiration, address(this)); - collateralization.lock(_id); - vm.warp(_expiration + 1); - collateralization.slash(_id, 1); - } - - function testFail_SlashUnlocked() public { - uint128 _expiration = uint128(block.timestamp) + 3; - token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(0, 1, _expiration, address(this)); - vm.warp(_expiration + 1); - collateralization.slash(_id, 1); - } - - function testFail_SlashFromNonArbiter() public { - uint128 _expiration = uint128(block.timestamp) + 3; - address _arbiter = 0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF; - token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(0, 1, _expiration, _arbiter); - vm.warp(_expiration - 1); - collateralization.slash(_id, 1); - } } diff --git a/test/Example.t.sol b/test/Example.t.sol index 5e4c81a..444fc7f 100644 --- a/test/Example.t.sol +++ b/test/Example.t.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.13; import {Test} from "forge-std/Test.sol"; import {ERC20Burnable} from "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.sol"; import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; -import {Collateralization, Deposit, DepositState, UnexpectedState} from "../src/Collateralization.sol"; +import {Collateralization, DepositState} from "../src/Collateralization.sol"; import {DataService} from "../src/examples/DataService.sol"; import {Lender, Limits} from "../src/examples/Lender.sol"; import { @@ -24,6 +24,8 @@ contract CollateralizationUnitTests is Test, ILender { LoanAggregator public aggregator; Lender public lender; + function onCollateralWithraw(uint256 _value, uint96 _lenderData) public {} + function setUp() public { token = new TestToken(1_000); collateralization = new Collateralization(token); @@ -33,8 +35,6 @@ contract CollateralizationUnitTests is Test, ILender { token.transfer(address(lender), 80); } - function onCollateralWithraw(uint256 _value, uint96 _lenderData) public {} - function test_Example() public { // Add this contract as a data service provider to receive 10 tokens in payment. token.approve(address(dataService), 10); @@ -53,8 +53,8 @@ contract CollateralizationUnitTests is Test, ILender { token.approve(address(lender), 6); _loanCommitments[1] = lender.borrow(80, 5, 1, dataService.disputePeriod()); // Receive 10 token payment and start dispute period. - uint128 _expiration = uint128(block.timestamp) + dataService.disputePeriod(); - uint128 _deposit = aggregator.remitPayment(DataService(dataService), _expiration, _loanCommitments); + uint64 _unlock = uint64(block.timestamp) + dataService.disputePeriod(); + uint128 _deposit = aggregator.remitPayment(DataService(dataService), _unlock, _loanCommitments); assertEq(token.balanceOf(address(this)), _initialBalance + 10 - 26); assertEq(token.balanceOf(address(lender)), _initialLenderBalance + 6 - 80); @@ -84,8 +84,8 @@ contract CollateralizationUnitTests is Test, ILender { token.approve(address(lender), 6); _loanCommitments[1] = lender.borrow(80, 5, 1, dataService.disputePeriod()); // Receive 10 token payment and start dispute period. - uint128 _expiration = uint128(block.timestamp) + dataService.disputePeriod(); - uint128 _deposit = aggregator.remitPayment(DataService(dataService), _expiration, _loanCommitments); + uint64 _unlock = uint64(block.timestamp) + dataService.disputePeriod(); + uint128 _deposit = aggregator.remitPayment(DataService(dataService), _unlock, _loanCommitments); assertEq(token.balanceOf(address(this)), _initialBalance + 10 - 26); assertEq(token.balanceOf(address(lender)), _initialLenderBalance + 6 - 80); From 2b14690aaa71c96da9ab5952bb864a83c8b2f3e2 Mon Sep 17 00:00:00 2001 From: Theo Butler Date: Tue, 1 Aug 2023 09:00:07 -0400 Subject: [PATCH 11/23] make functions external --- src/Collateralization.sol | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Collateralization.sol b/src/Collateralization.sol index 473bc68..4029350 100644 --- a/src/Collateralization.sol +++ b/src/Collateralization.sol @@ -70,7 +70,7 @@ contract Collateralization { /// @param _value Initial token value of the new deposit. /// @param _unlock Unlock timestamp of the new deposit, in seconds. Set to a nonzero value to lock deposit. /// @return id Unique ID associated with the new deposit. - function deposit(address _arbiter, uint256 _value, uint64 _unlock) public returns (uint128) { + function deposit(address _arbiter, uint256 _value, uint64 _unlock) external returns (uint128) { lastID += 1; deposits[lastID] = DepositState({ depositor: msg.sender, @@ -89,7 +89,7 @@ contract Collateralization { /// Lock the deposit associated with the given ID. This makes the deposit slashable until it is unlocked. /// @param _id ID of the associated deposit. /// @param _unlock Unlock timestamp of deposit, in seconds. - function lock(uint128 _id, uint64 _unlock) public { + function lock(uint128 _id, uint64 _unlock) external { DepositState memory _deposit = getDeposit(_id); require(msg.sender == _deposit.arbiter, "sender not arbiter"); require(_deposit.end == 0, "deposit withdrawn"); @@ -105,7 +105,7 @@ contract Collateralization { /// the deposit associated with the given ID. /// @param _id ID of the associated deposit. /// @param _amount Amount of remaining deposit tokens to burn. - function slash(uint128 _id, uint256 _amount) public { + function slash(uint128 _id, uint256 _amount) external { DepositState memory _deposit = getDeposit(_id); require(msg.sender == _deposit.arbiter, "sender not arbiter"); require(_deposit.end == 0, "deposit withdrawn"); @@ -118,7 +118,7 @@ contract Collateralization { /// Collect remaining tokens associated with a deposit. /// @param _id ID of the associated deposit. - function withdraw(uint128 _id) public { + function withdraw(uint128 _id) external { DepositState memory _deposit = getDeposit(_id); require(_deposit.depositor == msg.sender, "sender not depositor"); require(_deposit.end == 0, "deposit withdrawn"); @@ -139,7 +139,7 @@ contract Collateralization { /// Return true if the deposit associated with the given ID is slashable, false otherwise. /// @param _id ID of the associated deposit. - function isSlashable(uint128 _id) public view returns (bool) { + function isSlashable(uint128 _id) external view returns (bool) { DepositState memory _deposit = getDeposit(_id); return (block.timestamp < _deposit.unlock); } From 05995d605dc7477cd0b1c0d7b2a9e88ab5f8e7ad Mon Sep 17 00:00:00 2001 From: Theo Butler Date: Tue, 1 Aug 2023 09:01:06 -0400 Subject: [PATCH 12/23] add depositor to Deposit event --- src/Collateralization.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Collateralization.sol b/src/Collateralization.sol index 4029350..6d5e863 100644 --- a/src/Collateralization.sol +++ b/src/Collateralization.sol @@ -47,7 +47,7 @@ struct DepositState { /// This contract manages Deposits as described above. contract Collateralization { - event Deposit(uint128 indexed id, address indexed arbiter, uint256 value, uint64 unlock); + event Deposit(uint128 indexed id, address indexed depositor, address indexed arbiter, uint256 value, uint64 unlock); event Lock(uint128 indexed id, uint64 unlock); event Slash(uint128 indexed id, uint256 amount); event Withdraw(uint128 indexed id); @@ -82,7 +82,7 @@ contract Collateralization { }); bool _transferSuccess = token.transferFrom(msg.sender, address(this), _value); require(_transferSuccess, "transfer failed"); - emit Deposit(lastID, _arbiter, _value, _unlock); + emit Deposit(lastID, msg.sender, _arbiter, _value, _unlock); return lastID; } From fa4249fa5942ef2c7082016d58aa750daf0d28bd Mon Sep 17 00:00:00 2001 From: Theo Butler Date: Tue, 1 Aug 2023 09:03:31 -0400 Subject: [PATCH 13/23] make token address immutable --- src/Collateralization.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Collateralization.sol b/src/Collateralization.sol index 6d5e863..5413078 100644 --- a/src/Collateralization.sol +++ b/src/Collateralization.sol @@ -53,7 +53,7 @@ contract Collateralization { event Withdraw(uint128 indexed id); /// Burnable ERC-20 token held by this contract. - ERC20Burnable public token; + ERC20Burnable public immutable token; /// Mapping of deposit IDs to deposits. mapping(uint128 => DepositState) public deposits; /// Counter for assigning new deposit IDs. @@ -61,7 +61,6 @@ contract Collateralization { /// @param _token the burnable ERC-20 token held by this contract. constructor(ERC20Burnable _token) { - // TODO: use a constant token = _token; } From 82effe05116f104103b0d72190ec3a29f00c9f46 Mon Sep 17 00:00:00 2001 From: Theo Butler Date: Tue, 1 Aug 2023 09:08:51 -0400 Subject: [PATCH 14/23] move DepositState into Collateralization namespace --- src/Collateralization.sol | 86 ++++++++++++++++----------------- src/examples/DataService.sol | 4 +- src/examples/LoanAggregator.sol | 4 +- test/Collateralization.t.sol | 4 +- test/Example.t.sol | 2 +- 5 files changed, 50 insertions(+), 50 deletions(-) diff --git a/src/Collateralization.sol b/src/Collateralization.sol index 5413078..dd7276c 100644 --- a/src/Collateralization.sol +++ b/src/Collateralization.sol @@ -3,55 +3,55 @@ pragma solidity ^0.8.13; import "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.sol"; -/// The state associated with a slashable, potentially time-locked deposit of tokens. When a deposit is locked -/// (`block.timestamp < unlock`) it has the following properties: -/// - A deposit may only be withdrawn when the deposit is unlocked (`block.timestamp >= unlock`). Withdrawal returns the -/// deposit's token value to the depositor. -/// - The arbiter has authority to slash the deposit before unlock, which burns a given amount tokens. A slash also -/// reduces the tokens available to withdraw by the same amount. -struct DepositState { - // creator of the deposit, has ability to withdraw when the deposit is unlocked - address depositor; - // authority to slash deposit value, when the deposit is locked - address arbiter; - // token amount associated with deposit - uint256 value; - // timestamp when deposit is no longer locked - uint64 unlock; - // timestamp of deposit creation - uint64 start; - // timestamp of withdrawal, 0 until withdrawn - uint64 end; -} - -// ┌────────┐ ┌──────┐ ┌─────────┐ -// │unlocked│ │locked│ │withdrawn│ -// └───┬────┘ └──┬───┘ └────┬────┘ -// deposit (unlock == 0) │ │ │ -// ─────────────────────>│ │ │ -// │ │ │ -// deposit (unlock != 0) │ │ -// ───────────────────────────────────────────────────────>│ │ -// │ │ │ -// │ lock (block.timestamp < _unlock)│ │ -// │ ───────────────────────────────>│ │ -// │ │ │ -// │ (block.timestamp >= unlock) │ │ -// │ <─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│ │ -// │ │ │ -// │ withdraw │ │ -// │ ───────────────────────────────────────────────────>│ -// ┌───┴────┐ ┌──┴───┐ ┌────┴────┐ -// │unlocked│ │locked│ │withdrawn│ -// └────────┘ └──────┘ └─────────┘ - -/// This contract manages Deposits as described above. +/// This contract manages slashable, potentially time-locked token deposits. contract Collateralization { event Deposit(uint128 indexed id, address indexed depositor, address indexed arbiter, uint256 value, uint64 unlock); event Lock(uint128 indexed id, uint64 unlock); event Slash(uint128 indexed id, uint256 amount); event Withdraw(uint128 indexed id); + /// The state associated with a deposit. When a deposit is locked (`block.timestamp < unlock`) it has the following + /// properties: + /// - A deposit may only be withdrawn when the deposit is unlocked (`block.timestamp >= unlock`). Withdrawal returns the + /// deposit's token value to the depositor. + /// - The arbiter has authority to slash the deposit before unlock, which burns a given amount tokens. A slash also + /// reduces the tokens available to withdraw by the same amount. + struct DepositState { + // creator of the deposit, has ability to withdraw when the deposit is unlocked + address depositor; + // authority to slash deposit value, when the deposit is locked + address arbiter; + // token amount associated with deposit + uint256 value; + // timestamp when deposit is no longer locked + uint64 unlock; + // timestamp of deposit creation + uint64 start; + // timestamp of withdrawal, 0 until withdrawn + uint64 end; + } + + // ┌────────┐ ┌──────┐ ┌─────────┐ + // │unlocked│ │locked│ │withdrawn│ + // └───┬────┘ └──┬───┘ └────┬────┘ + // deposit (unlock == 0) │ │ │ + // ─────────────────────>│ │ │ + // │ │ │ + // deposit (unlock != 0) │ │ + // ───────────────────────────────────────────────────────>│ │ + // │ │ │ + // │ lock (block.timestamp < _unlock)│ │ + // │ ───────────────────────────────>│ │ + // │ │ │ + // │ (block.timestamp >= unlock) │ │ + // │ <─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│ │ + // │ │ │ + // │ withdraw │ │ + // │ ───────────────────────────────────────────────────>│ + // ┌───┴────┐ ┌──┴───┐ ┌────┴────┐ + // │unlocked│ │locked│ │withdrawn│ + // └────────┘ └──────┘ └─────────┘ + /// Burnable ERC-20 token held by this contract. ERC20Burnable public immutable token; /// Mapping of deposit IDs to deposits. diff --git a/src/examples/DataService.sol b/src/examples/DataService.sol index a330c4a..e29d076 100644 --- a/src/examples/DataService.sol +++ b/src/examples/DataService.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.13; import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol"; -import {Collateralization, DepositState} from "../Collateralization.sol"; +import {Collateralization} from "../Collateralization.sol"; import {IDataService} from "./LoanAggregator.sol"; contract DataService is Ownable, IDataService { @@ -43,7 +43,7 @@ contract DataService is Ownable, IDataService { /// Called by data service provider to receive payment. This locks the given deposit to begin a dispute period. function remitPayment(address _providerAddr, uint128 _depositID, uint64 _unlock) public { ProviderState memory _provider = getProviderState(_providerAddr); - DepositState memory _deposit = collateralization.getDeposit(_depositID); + Collateralization.DepositState memory _deposit = collateralization.getDeposit(_depositID); uint256 minCollateral = uint256(_provider.payment) * 10; require(_deposit.value >= minCollateral, "collateral below minimum"); diff --git a/src/examples/LoanAggregator.sol b/src/examples/LoanAggregator.sol index 1820177..ac7c37a 100644 --- a/src/examples/LoanAggregator.sol +++ b/src/examples/LoanAggregator.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.13; -import {Collateralization, DepositState} from "../Collateralization.sol"; +import {Collateralization} from "../Collateralization.sol"; interface IDataService { function remitPayment(address _provider, uint128 _deposit, uint64 _unlock) external; @@ -57,7 +57,7 @@ contract LoanAggregator { } function withdraw(uint128 _depositID) public { - DepositState memory _deposit = collateralization.getDeposit(_depositID); + Collateralization.DepositState memory _deposit = collateralization.getDeposit(_depositID); collateralization.withdraw(_depositID); // calculate original deposit value uint256 _index = 0; diff --git a/test/Collateralization.t.sol b/test/Collateralization.t.sol index 348d331..d15a3cb 100644 --- a/test/Collateralization.t.sol +++ b/test/Collateralization.t.sol @@ -6,7 +6,7 @@ import {StdUtils} from "forge-std/StdUtils.sol"; import {Test} from "forge-std/Test.sol"; import {ERC20Burnable} from "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.sol"; import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; -import {Collateralization, DepositState} from "../src/Collateralization.sol"; +import {Collateralization} from "../src/Collateralization.sol"; contract TestToken is ERC20Burnable { constructor(uint256 _initialSupply) ERC20("MockCoin", "MOCK") { @@ -72,7 +72,7 @@ contract CollateralizationHandler is CommonBase, StdUtils { uint64 _index = 0; while (_index < depositIDs.length) { uint128 _id = depositIDs[_index]; - DepositState memory _deposit = collateralization.getDeposit(_id); + Collateralization.DepositState memory _deposit = collateralization.getDeposit(_id); if (_deposit.depositor != address(0)) { total += _deposit.value; } diff --git a/test/Example.t.sol b/test/Example.t.sol index 444fc7f..45eecf2 100644 --- a/test/Example.t.sol +++ b/test/Example.t.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.13; import {Test} from "forge-std/Test.sol"; import {ERC20Burnable} from "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.sol"; import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; -import {Collateralization, DepositState} from "../src/Collateralization.sol"; +import {Collateralization} from "../src/Collateralization.sol"; import {DataService} from "../src/examples/DataService.sol"; import {Lender, Limits} from "../src/examples/Lender.sol"; import { From b96f3fc23d65bac45a638d80e6aeedb96130e010 Mon Sep 17 00:00:00 2001 From: Theo Butler Date: Tue, 1 Aug 2023 09:16:23 -0400 Subject: [PATCH 15/23] rename: value -> amount --- src/Collateralization.sol | 26 +++++++++++++------------- src/examples/DataService.sol | 2 +- src/examples/Lender.sol | 28 ++++++++++++++-------------- src/examples/LoanAggregator.sol | 26 +++++++++++++------------- test/Collateralization.t.sol | 15 +++++++++------ test/Example.t.sol | 8 ++++---- 6 files changed, 54 insertions(+), 51 deletions(-) diff --git a/src/Collateralization.sol b/src/Collateralization.sol index dd7276c..f9ac803 100644 --- a/src/Collateralization.sol +++ b/src/Collateralization.sol @@ -5,7 +5,7 @@ import "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.so /// This contract manages slashable, potentially time-locked token deposits. contract Collateralization { - event Deposit(uint128 indexed id, address indexed depositor, address indexed arbiter, uint256 value, uint64 unlock); + event Deposit(uint128 indexed id, address indexed depositor, address indexed arbiter, uint256 amount, uint64 unlock); event Lock(uint128 indexed id, uint64 unlock); event Slash(uint128 indexed id, uint256 amount); event Withdraw(uint128 indexed id); @@ -13,16 +13,16 @@ contract Collateralization { /// The state associated with a deposit. When a deposit is locked (`block.timestamp < unlock`) it has the following /// properties: /// - A deposit may only be withdrawn when the deposit is unlocked (`block.timestamp >= unlock`). Withdrawal returns the - /// deposit's token value to the depositor. + /// deposit's token amount to the depositor. /// - The arbiter has authority to slash the deposit before unlock, which burns a given amount tokens. A slash also /// reduces the tokens available to withdraw by the same amount. struct DepositState { // creator of the deposit, has ability to withdraw when the deposit is unlocked address depositor; - // authority to slash deposit value, when the deposit is locked + // authority to slash deposit amount, when the deposit is locked address arbiter; // token amount associated with deposit - uint256 value; + uint256 amount; // timestamp when deposit is no longer locked uint64 unlock; // timestamp of deposit creation @@ -66,22 +66,22 @@ contract Collateralization { /// Create a new deposit, returning its associated ID. /// @param _arbiter Arbiter of the new deposit. - /// @param _value Initial token value of the new deposit. + /// @param _amount Initial token amount of the new deposit. /// @param _unlock Unlock timestamp of the new deposit, in seconds. Set to a nonzero value to lock deposit. /// @return id Unique ID associated with the new deposit. - function deposit(address _arbiter, uint256 _value, uint64 _unlock) external returns (uint128) { + function deposit(address _arbiter, uint256 _amount, uint64 _unlock) external returns (uint128) { lastID += 1; deposits[lastID] = DepositState({ depositor: msg.sender, arbiter: _arbiter, - value: _value, + amount: _amount, unlock: _unlock, start: uint64(block.timestamp), end: 0 }); - bool _transferSuccess = token.transferFrom(msg.sender, address(this), _value); + bool _transferSuccess = token.transferFrom(msg.sender, address(this), _amount); require(_transferSuccess, "transfer failed"); - emit Deposit(lastID, msg.sender, _arbiter, _value, _unlock); + emit Deposit(lastID, msg.sender, _arbiter, _amount, _unlock); return lastID; } @@ -100,7 +100,7 @@ contract Collateralization { emit Lock(_id, _unlock); } - /// Burn some amount of the deposit value while it's locked. This action can only be performed by the arbiter of + /// Burn some of the deposit amount while it's locked. This action can only be performed by the arbiter of /// the deposit associated with the given ID. /// @param _id ID of the associated deposit. /// @param _amount Amount of remaining deposit tokens to burn. @@ -109,8 +109,8 @@ contract Collateralization { require(msg.sender == _deposit.arbiter, "sender not arbiter"); require(_deposit.end == 0, "deposit withdrawn"); require(block.timestamp < _deposit.unlock, "deposit unlocked"); - require(_amount <= _deposit.value, "amount too large"); - deposits[_id].value -= _amount; + require(_amount <= _deposit.amount, "amount too large"); + deposits[_id].amount -= _amount; token.burn(_amount); emit Slash(_id, _amount); } @@ -123,7 +123,7 @@ contract Collateralization { require(_deposit.end == 0, "deposit withdrawn"); require(block.timestamp >= _deposit.unlock, "deposit locked"); deposits[_id].end = uint64(block.timestamp); - bool _transferSuccess = token.transfer(_deposit.depositor, _deposit.value); + bool _transferSuccess = token.transfer(_deposit.depositor, _deposit.amount); require(_transferSuccess, "transfer failed"); emit Withdraw(_id); } diff --git a/src/examples/DataService.sol b/src/examples/DataService.sol index e29d076..b62c0f8 100644 --- a/src/examples/DataService.sol +++ b/src/examples/DataService.sol @@ -46,7 +46,7 @@ contract DataService is Ownable, IDataService { Collateralization.DepositState memory _deposit = collateralization.getDeposit(_depositID); uint256 minCollateral = uint256(_provider.payment) * 10; - require(_deposit.value >= minCollateral, "collateral below minimum"); + require(_deposit.amount >= minCollateral, "collateral below minimum"); uint128 disputePeriodEnd = uint128(block.timestamp + disputePeriod); require(_unlock >= disputePeriodEnd, "collateral unlock before end of dispute period"); diff --git a/src/examples/Lender.sol b/src/examples/Lender.sol index ee78e6c..c52348d 100644 --- a/src/examples/Lender.sol +++ b/src/examples/Lender.sol @@ -6,14 +6,14 @@ import {SafeCast} from "openzeppelin-contracts/contracts/utils/math/SafeCast.sol import {AggregatedLoan, ILender, LoanAggregator, LoanCommitment} from "./LoanAggregator.sol"; struct Limits { - uint256 maxValue; + uint256 maxAmount; uint64 maxDuration; } contract Lender is Ownable, ILender { struct LoanState { address borrower; - uint256 initialValue; + uint256 initialAmount; uint256 borrowerCollateral; } @@ -30,31 +30,31 @@ contract Lender is Ownable, ILender { return agg.collateralization().token().transfer(owner(), _amount); } - function borrow(uint256 _value, uint256 _collateral, uint256 _payment, uint64 _unlock) + function borrow(uint256 _amount, uint256 _collateral, uint256 _payment, uint64 _unlock) public returns (LoanCommitment memory) { - require(_collateral <= _value, "collateral > value"); + require(_collateral <= _amount, "collateral > amount"); uint64 _duration = SafeCast.toUint64(_unlock - block.timestamp); require(_duration <= limits.maxDuration, "duration over maximum"); - require(_value <= limits.maxValue, "value over maximum"); - require(_payment >= expectedPayment(_value, _duration), "payment below expected"); + require(_amount <= limits.maxAmount, "amount over maximum"); + require(_payment >= expectedPayment(_amount, _duration), "payment below expected"); uint256 _transferAmount = _collateral + _payment; agg.collateralization().token().transferFrom(msg.sender, address(this), _transferAmount); uint96 _loanIndex = uint96(loans.length); - loans.push(LoanState({borrower: msg.sender, initialValue: _value, borrowerCollateral: _collateral})); - agg.collateralization().token().approve(address(agg), _value); + loans.push(LoanState({borrower: msg.sender, initialAmount: _amount, borrowerCollateral: _collateral})); + agg.collateralization().token().approve(address(agg), _amount); return LoanCommitment({ - loan: AggregatedLoan({lender: this, lenderData: _loanIndex, value: _value}), + loan: AggregatedLoan({lender: this, lenderData: _loanIndex, amount: _amount}), signature: "siggy" }); } - /// Return the expected payment for a loan, based on its value and duration. - /// @param _value Deposit value. + /// Return the expected payment for a loan, based on its amount and duration. + /// @param _amount Deposit amount. /// @param _duration Deposit duration from the block at which the deposit is funded, in seconds. - function expectedPayment(uint256 _value, uint64 _duration) public view returns (uint256) { + function expectedPayment(uint256 _amount, uint64 _duration) public view returns (uint256) { // TODO: Now for the tricky bit! // Ideally the owner would be able to set an expected annualized return. However, I did not // figure out an obvious way to calculate the annualized return given the loan duration @@ -63,10 +63,10 @@ contract Lender is Ownable, ILender { return 1; } - function onCollateralWithraw(uint256 _value, uint96 _lenderData) public { + function onCollateralWithraw(uint256 _amount, uint96 _lenderData) public { LoanState memory _loan = loans[_lenderData]; delete loans[_lenderData]; - uint256 _loss = _loan.initialValue - _value; + uint256 _loss = _loan.initialAmount - _amount; if (_loss < _loan.borrowerCollateral) { agg.collateralization().token().transfer(_loan.borrower, _loan.borrowerCollateral - _loss); } diff --git a/src/examples/LoanAggregator.sol b/src/examples/LoanAggregator.sol index ac7c37a..025ec14 100644 --- a/src/examples/LoanAggregator.sol +++ b/src/examples/LoanAggregator.sol @@ -8,7 +8,7 @@ interface IDataService { } interface ILender { - function onCollateralWithraw(uint256 _value, uint96 _lenderData) external; + function onCollateralWithraw(uint256 _amount, uint96 _lenderData) external; } struct LoanCommitment { @@ -19,7 +19,7 @@ struct LoanCommitment { struct AggregatedLoan { ILender lender; uint96 lenderData; - uint256 value; + uint256 amount; } contract LoanAggregator { @@ -35,18 +35,18 @@ contract LoanAggregator { returns (uint128) { uint256 _index = 0; - uint256 _value = 0; + uint256 _amount = 0; while (_index < _loanCommitments.length) { LoanCommitment memory _commitment = _loanCommitments[_index]; - // TODO: verify signature of (lender, value, arbiter, unlock) - _value += _commitment.loan.value; + // TODO: verify signature of (lender, amount, arbiter, unlock) + _amount += _commitment.loan.amount; collateralization.token().transferFrom( - address(_commitment.loan.lender), address(this), _commitment.loan.value + address(_commitment.loan.lender), address(this), _commitment.loan.amount ); _index += 1; } - collateralization.token().approve(address(collateralization), _value); - uint128 _deposit = collateralization.deposit(address(_arbiter), _value, _unlock); + collateralization.token().approve(address(collateralization), _amount); + uint128 _deposit = collateralization.deposit(address(_arbiter), _amount, _unlock); _index = 0; while (_index < _loanCommitments.length) { loans[_deposit].push(_loanCommitments[_index].loan); @@ -59,18 +59,18 @@ contract LoanAggregator { function withdraw(uint128 _depositID) public { Collateralization.DepositState memory _deposit = collateralization.getDeposit(_depositID); collateralization.withdraw(_depositID); - // calculate original deposit value + // calculate original deposit amount uint256 _index = 0; - uint256 _initialValue = 0; + uint256 _initialAmount = 0; while (_index < loans[_depositID].length) { - _initialValue += loans[_depositID][_index].value; + _initialAmount += loans[_depositID][_index].amount; _index += 1; } - // distribute remaining deposit value back to lenders + // distribute remaining deposit amount back to lenders _index = 0; while (_index < loans[_depositID].length) { AggregatedLoan memory _loan = loans[_depositID][_index]; - uint256 _lenderReturn = (_loan.value * _deposit.value) / _initialValue; + uint256 _lenderReturn = (_loan.amount * _deposit.amount) / _initialAmount; collateralization.token().transfer(address(_loan.lender), _lenderReturn); _loan.lender.onCollateralWithraw(_lenderReturn, _loan.lenderData); _index += 1; diff --git a/test/Collateralization.t.sol b/test/Collateralization.t.sol index d15a3cb..e57c389 100644 --- a/test/Collateralization.t.sol +++ b/test/Collateralization.t.sol @@ -36,12 +36,15 @@ contract CollateralizationHandler is CommonBase, StdUtils { vm.warp(block.timestamp + bound(blocks, 1, 10)); } - function deposit(uint256 __sender, uint256 __arbiter, uint256 __value, uint256 __unlock) public returns (uint128) { + function deposit(uint256 __sender, uint256 __arbiter, uint256 __amount, uint256 __unlock) + public + returns (uint128) + { address _depositor = _genActor(__sender); - uint256 _value = bound(__value, 1, collateralization.token().balanceOf(_depositor)); + uint256 _amount = bound(__amount, 1, collateralization.token().balanceOf(_depositor)); vm.startPrank(_depositor); - collateralization.token().approve(address(collateralization), _value); - uint128 _id = collateralization.deposit(_genActor(__arbiter), _value, _genTimestamp(__unlock)); + collateralization.token().approve(address(collateralization), _amount); + uint128 _id = collateralization.deposit(_genActor(__arbiter), _amount, _genTimestamp(__unlock)); vm.stopPrank(); depositIDs.push(_id); return _id; @@ -62,7 +65,7 @@ contract CollateralizationHandler is CommonBase, StdUtils { function slash(uint256 __sender, uint256 __id, uint256 __amount) public { uint128 _id = _genID(__id); vm.prank(_genActor(__sender)); - collateralization.slash(_id, bound(__amount, 0, collateralization.getDeposit(_id).value)); + collateralization.slash(_id, bound(__amount, 0, collateralization.getDeposit(_id).amount)); assert(collateralization.isSlashable(_id)); _removeDepositID(_id); } @@ -74,7 +77,7 @@ contract CollateralizationHandler is CommonBase, StdUtils { uint128 _id = depositIDs[_index]; Collateralization.DepositState memory _deposit = collateralization.getDeposit(_id); if (_deposit.depositor != address(0)) { - total += _deposit.value; + total += _deposit.amount; } _index += 1; } diff --git a/test/Example.t.sol b/test/Example.t.sol index 45eecf2..62644e7 100644 --- a/test/Example.t.sol +++ b/test/Example.t.sol @@ -24,14 +24,14 @@ contract CollateralizationUnitTests is Test, ILender { LoanAggregator public aggregator; Lender public lender; - function onCollateralWithraw(uint256 _value, uint96 _lenderData) public {} + function onCollateralWithraw(uint256 _amount, uint96 _lenderData) public {} function setUp() public { token = new TestToken(1_000); collateralization = new Collateralization(token); aggregator = new LoanAggregator(collateralization); dataService = new DataService(collateralization, 20 days); - lender = new Lender(aggregator, Limits({maxValue: 100, maxDuration: 30 days})); + lender = new Lender(aggregator, Limits({maxAmount: 100, maxDuration: 30 days})); token.transfer(address(lender), 80); } @@ -48,7 +48,7 @@ contract CollateralizationUnitTests is Test, ILender { LoanCommitment[] memory _loanCommitments = new LoanCommitment[](2); token.approve(address(aggregator), 20); _loanCommitments[0] = - LoanCommitment({loan: AggregatedLoan({lender: this, lenderData: 0, value: 20}), signature: "siggy"}); + LoanCommitment({loan: AggregatedLoan({lender: this, lenderData: 0, amount: 20}), signature: "siggy"}); // Send lender 5 tokens in collateral and 1 token in payment (6 total) for a 80 token loan. token.approve(address(lender), 6); _loanCommitments[1] = lender.borrow(80, 5, 1, dataService.disputePeriod()); @@ -79,7 +79,7 @@ contract CollateralizationUnitTests is Test, ILender { LoanCommitment[] memory _loanCommitments = new LoanCommitment[](2); token.approve(address(aggregator), 20); _loanCommitments[0] = - LoanCommitment({loan: AggregatedLoan({lender: this, lenderData: 0, value: 20}), signature: "siggy"}); + LoanCommitment({loan: AggregatedLoan({lender: this, lenderData: 0, amount: 20}), signature: "siggy"}); // Send lender 5 tokens in collateral and 1 token in payment (6 total) for a 80 token loan. token.approve(address(lender), 6); _loanCommitments[1] = lender.borrow(80, 5, 1, dataService.disputePeriod()); From 452979c793e06760d877d654f07e2c5a47ffb984 Mon Sep 17 00:00:00 2001 From: Theo Butler Date: Tue, 1 Aug 2023 09:28:49 -0400 Subject: [PATCH 16/23] lock: revert when unlock is set --- src/Collateralization.sol | 7 +++---- src/examples/DataService.sol | 4 +++- test/Collateralization.t.sol | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Collateralization.sol b/src/Collateralization.sol index f9ac803..b59f13a 100644 --- a/src/Collateralization.sol +++ b/src/Collateralization.sol @@ -5,7 +5,9 @@ import "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.so /// This contract manages slashable, potentially time-locked token deposits. contract Collateralization { - event Deposit(uint128 indexed id, address indexed depositor, address indexed arbiter, uint256 amount, uint64 unlock); + event Deposit( + uint128 indexed id, address indexed depositor, address indexed arbiter, uint256 amount, uint64 unlock + ); event Lock(uint128 indexed id, uint64 unlock); event Slash(uint128 indexed id, uint256 amount); event Withdraw(uint128 indexed id); @@ -92,9 +94,6 @@ contract Collateralization { DepositState memory _deposit = getDeposit(_id); require(msg.sender == _deposit.arbiter, "sender not arbiter"); require(_deposit.end == 0, "deposit withdrawn"); - if (_deposit.unlock == _unlock) { - return; - } require(_deposit.unlock == 0, "deposit locked"); deposits[_id].unlock = _unlock; emit Lock(_id, _unlock); diff --git a/src/examples/DataService.sol b/src/examples/DataService.sol index b62c0f8..ce82a98 100644 --- a/src/examples/DataService.sol +++ b/src/examples/DataService.sol @@ -51,7 +51,9 @@ contract DataService is Ownable, IDataService { require(_unlock >= disputePeriodEnd, "collateral unlock before end of dispute period"); providers[_providerAddr].deposit = _depositID; - collateralization.lock(_depositID, _unlock); + if (_deposit.unlock == 0) { + collateralization.lock(_depositID, _unlock); + } collateralization.token().transfer(_providerAddr, _provider.payment); } diff --git a/test/Collateralization.t.sol b/test/Collateralization.t.sol index e57c389..55b8e40 100644 --- a/test/Collateralization.t.sol +++ b/test/Collateralization.t.sol @@ -170,7 +170,7 @@ contract CollateralizationUnitTests is Test { assertEq(collateralization.isSlashable(_id), true); } - function test_LockLocked() public { + function testFail_LockLocked() public { uint64 _unlock = uint64(block.timestamp) + 1; token.approve(address(collateralization), 1); uint128 _id = collateralization.deposit(address(this), 1, _unlock); From c708eeb57bedf3ad63f324fa8283d67bbcf66b77 Mon Sep 17 00:00:00 2001 From: Theo Butler Date: Tue, 1 Aug 2023 09:41:04 -0400 Subject: [PATCH 17/23] add multicall --- src/Collateralization.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Collateralization.sol b/src/Collateralization.sol index b59f13a..1c0eb6a 100644 --- a/src/Collateralization.sol +++ b/src/Collateralization.sol @@ -2,9 +2,10 @@ pragma solidity ^0.8.13; import "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.sol"; +import "openzeppelin-contracts/contracts/utils/Multicall.sol"; /// This contract manages slashable, potentially time-locked token deposits. -contract Collateralization { +contract Collateralization is Multicall { event Deposit( uint128 indexed id, address indexed depositor, address indexed arbiter, uint256 amount, uint64 unlock ); From 033ab68c5b630b5c0bf40501f45f8b3f40b9e1a4 Mon Sep 17 00:00:00 2001 From: Theo Butler Date: Thu, 3 Aug 2023 10:32:17 -0400 Subject: [PATCH 18/23] update docs --- src/Collateralization.sol | 51 +++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/src/Collateralization.sol b/src/Collateralization.sol index 1c0eb6a..56a9cc4 100644 --- a/src/Collateralization.sol +++ b/src/Collateralization.sol @@ -15,8 +15,8 @@ contract Collateralization is Multicall { /// The state associated with a deposit. When a deposit is locked (`block.timestamp < unlock`) it has the following /// properties: - /// - A deposit may only be withdrawn when the deposit is unlocked (`block.timestamp >= unlock`). Withdrawal returns the - /// deposit's token amount to the depositor. + /// - A deposit may only be withdrawn when the deposit is unlocked (`block.timestamp >= unlock`). Withdrawal returns + /// the deposit's token amount to the depositor. /// - The arbiter has authority to slash the deposit before unlock, which burns a given amount tokens. A slash also /// reduces the tokens available to withdraw by the same amount. struct DepositState { @@ -34,26 +34,26 @@ contract Collateralization is Multicall { uint64 end; } - // ┌────────┐ ┌──────┐ ┌─────────┐ - // │unlocked│ │locked│ │withdrawn│ - // └───┬────┘ └──┬───┘ └────┬────┘ - // deposit (unlock == 0) │ │ │ - // ─────────────────────>│ │ │ - // │ │ │ - // deposit (unlock != 0) │ │ - // ───────────────────────────────────────────────────────>│ │ - // │ │ │ - // │ lock (block.timestamp < _unlock)│ │ - // │ ───────────────────────────────>│ │ - // │ │ │ - // │ (block.timestamp >= unlock) │ │ - // │ <─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│ │ - // │ │ │ - // │ withdraw │ │ - // │ ───────────────────────────────────────────────────>│ - // ┌───┴────┐ ┌──┴───┐ ┌────┴────┐ - // │unlocked│ │locked│ │withdrawn│ - // └────────┘ └──────┘ └─────────┘ + // ┌────────┐ ┌──────┐ ┌─────────┐ + // │unlocked│ │locked│ │withdrawn│ + // └───┬────┘ └──┬───┘ └────┬────┘ + // deposit (_unlock == 0) │ │ │ + // ──────────────────────>│ │ │ + // │ │ │ + // deposit (block.timestamp < _unlock) │ │ + // ──────────────────────────────────────────────────────────>│ │ + // │ │ │ + // │ lock (block.timestamp < _unlock) │ │ + // │ ─────────────────────────────────>│ │ + // │ │ │ + // │ (block.timestamp >= unlock) │ │ + // │ <─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│ │ + // │ │ │ + // │ withdraw │ │ + // │ ─────────────────────────────────────────────────────>│ + // ┌───┴────┐ ┌──┴───┐ ┌────┴────┐ + // │unlocked│ │locked│ │withdrawn│ + // └────────┘ └──────┘ └─────────┘ /// Burnable ERC-20 token held by this contract. ERC20Burnable public immutable token; @@ -88,7 +88,8 @@ contract Collateralization is Multicall { return lastID; } - /// Lock the deposit associated with the given ID. This makes the deposit slashable until it is unlocked. + /// Lock the deposit associated with the given ID. This makes the deposit slashable until it is unlocked. This + /// modification to a deposit can only made when its unlock timestamp is unset (has a value of zero). /// @param _id ID of the associated deposit. /// @param _unlock Unlock timestamp of deposit, in seconds. function lock(uint128 _id, uint64 _unlock) external { @@ -136,7 +137,9 @@ contract Collateralization is Multicall { return _deposit; } - /// Return true if the deposit associated with the given ID is slashable, false otherwise. + /// Return true if the deposit associated with the given ID is slashable, false otherwise. A slashable deposit is + /// locked (`block.timestamp < unlock`). As the name suggests, a slashable deposit may be slashed, and cannot be + /// withdrawn by the depositor until it is unlocked (`block.timestamp >= unlock`). /// @param _id ID of the associated deposit. function isSlashable(uint128 _id) external view returns (bool) { DepositState memory _deposit = getDeposit(_id); From 204fa2abf515d6d6426656f745d5fae035d471d6 Mon Sep 17 00:00:00 2001 From: Theo Butler Date: Thu, 3 Aug 2023 11:14:37 -0400 Subject: [PATCH 19/23] make IDs more predictable --- src/Collateralization.sol | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/Collateralization.sol b/src/Collateralization.sol index 56a9cc4..f03572e 100644 --- a/src/Collateralization.sol +++ b/src/Collateralization.sol @@ -59,8 +59,8 @@ contract Collateralization is Multicall { ERC20Burnable public immutable token; /// Mapping of deposit IDs to deposits. mapping(uint128 => DepositState) public deposits; - /// Counter for assigning new deposit IDs. - uint128 public lastID; + /// Counters for generating new deposit IDs. + mapping(address => uint64) public nonces; /// @param _token the burnable ERC-20 token held by this contract. constructor(ERC20Burnable _token) { @@ -73,8 +73,9 @@ contract Collateralization is Multicall { /// @param _unlock Unlock timestamp of the new deposit, in seconds. Set to a nonzero value to lock deposit. /// @return id Unique ID associated with the new deposit. function deposit(address _arbiter, uint256 _amount, uint64 _unlock) external returns (uint128) { - lastID += 1; - deposits[lastID] = DepositState({ + uint64 _nonce = nonces[msg.sender]++; + uint128 _id = uint128(bytes16(keccak256(abi.encode(msg.sender, _nonce)))); + deposits[_id] = DepositState({ depositor: msg.sender, arbiter: _arbiter, amount: _amount, @@ -84,8 +85,8 @@ contract Collateralization is Multicall { }); bool _transferSuccess = token.transferFrom(msg.sender, address(this), _amount); require(_transferSuccess, "transfer failed"); - emit Deposit(lastID, msg.sender, _arbiter, _amount, _unlock); - return lastID; + emit Deposit(_id, msg.sender, _arbiter, _amount, _unlock); + return _id; } /// Lock the deposit associated with the given ID. This makes the deposit slashable until it is unlocked. This @@ -145,4 +146,10 @@ contract Collateralization is Multicall { DepositState memory _deposit = getDeposit(_id); return (block.timestamp < _deposit.unlock); } + + /// Return the next deposit ID for the given depositor address. + function nextID(address _depositor) external view returns (uint128) { + uint64 _nonce = nonces[_depositor] + 1; + return uint128(bytes16(keccak256(abi.encode(msg.sender, _nonce)))); + } } From 1bb91cf0aeaaf822b9294e5417ee5195da62c390 Mon Sep 17 00:00:00 2001 From: Theo Butler Date: Thu, 3 Aug 2023 13:57:51 -0400 Subject: [PATCH 20/23] update diagram --- src/Collateralization.sol | 40 +++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/Collateralization.sol b/src/Collateralization.sol index f03572e..33a7511 100644 --- a/src/Collateralization.sol +++ b/src/Collateralization.sol @@ -34,26 +34,26 @@ contract Collateralization is Multicall { uint64 end; } - // ┌────────┐ ┌──────┐ ┌─────────┐ - // │unlocked│ │locked│ │withdrawn│ - // └───┬────┘ └──┬───┘ └────┬────┘ - // deposit (_unlock == 0) │ │ │ - // ──────────────────────>│ │ │ - // │ │ │ - // deposit (block.timestamp < _unlock) │ │ - // ──────────────────────────────────────────────────────────>│ │ - // │ │ │ - // │ lock (block.timestamp < _unlock) │ │ - // │ ─────────────────────────────────>│ │ - // │ │ │ - // │ (block.timestamp >= unlock) │ │ - // │ <─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│ │ - // │ │ │ - // │ withdraw │ │ - // │ ─────────────────────────────────────────────────────>│ - // ┌───┴────┐ ┌──┴───┐ ┌────┴────┐ - // │unlocked│ │locked│ │withdrawn│ - // └────────┘ └──────┘ └─────────┘ + // ┌────────┐ ┌──────┐ ┌─────────┐ + // │unlocked│ │locked│ │withdrawn│ + // └───┬────┘ └──┬───┘ └────┬────┘ + // deposit (block.timestamp >= unlock) │ │ │ + // ───────────────────────────────────>│ │ │ + // │ │ │ + // deposit (block.timestamp < unlock) │ │ │ + // ───────────────────────────────────────────────────────────────────────>│ │ + // │ │ │ + // │ lock (block.timestamp < unlock) │ │ + // │ ─────────────────────────────────>│ │ + // │ │ │ + // │ (block.timestamp >= unlock) │ │ + // │ <─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│ │ + // │ │ │ + // │ withdraw │ │ + // │ ─────────────────────────────────────────────────────>│ + // ┌───┴────┐ ┌──┴───┐ ┌────┴────┐ + // │unlocked│ │locked│ │withdrawn│ + // └────────┘ └──────┘ └─────────┘ /// Burnable ERC-20 token held by this contract. ERC20Burnable public immutable token; From 3828ed9d23f44a1201e453dd2c993e2fbd5509fd Mon Sep 17 00:00:00 2001 From: Theo Butler Date: Wed, 9 Aug 2023 09:11:46 -0400 Subject: [PATCH 21/23] rename: getDeposit -> getDepositState --- src/Collateralization.sol | 10 +++++----- src/examples/DataService.sol | 2 +- src/examples/LoanAggregator.sol | 2 +- test/Collateralization.t.sol | 10 +++++----- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Collateralization.sol b/src/Collateralization.sol index 33a7511..a0e3cd8 100644 --- a/src/Collateralization.sol +++ b/src/Collateralization.sol @@ -94,7 +94,7 @@ contract Collateralization is Multicall { /// @param _id ID of the associated deposit. /// @param _unlock Unlock timestamp of deposit, in seconds. function lock(uint128 _id, uint64 _unlock) external { - DepositState memory _deposit = getDeposit(_id); + DepositState memory _deposit = getDepositState(_id); require(msg.sender == _deposit.arbiter, "sender not arbiter"); require(_deposit.end == 0, "deposit withdrawn"); require(_deposit.unlock == 0, "deposit locked"); @@ -107,7 +107,7 @@ contract Collateralization is Multicall { /// @param _id ID of the associated deposit. /// @param _amount Amount of remaining deposit tokens to burn. function slash(uint128 _id, uint256 _amount) external { - DepositState memory _deposit = getDeposit(_id); + DepositState memory _deposit = getDepositState(_id); require(msg.sender == _deposit.arbiter, "sender not arbiter"); require(_deposit.end == 0, "deposit withdrawn"); require(block.timestamp < _deposit.unlock, "deposit unlocked"); @@ -120,7 +120,7 @@ contract Collateralization is Multicall { /// Collect remaining tokens associated with a deposit. /// @param _id ID of the associated deposit. function withdraw(uint128 _id) external { - DepositState memory _deposit = getDeposit(_id); + DepositState memory _deposit = getDepositState(_id); require(_deposit.depositor == msg.sender, "sender not depositor"); require(_deposit.end == 0, "deposit withdrawn"); require(block.timestamp >= _deposit.unlock, "deposit locked"); @@ -132,7 +132,7 @@ contract Collateralization is Multicall { /// Return the deposit state associated with the given ID. /// @param _id ID of the associated deposit. - function getDeposit(uint128 _id) public view returns (DepositState memory) { + function getDepositState(uint128 _id) public view returns (DepositState memory) { DepositState memory _deposit = deposits[_id]; require(_deposit.depositor != address(0), "deposit not found"); return _deposit; @@ -143,7 +143,7 @@ contract Collateralization is Multicall { /// withdrawn by the depositor until it is unlocked (`block.timestamp >= unlock`). /// @param _id ID of the associated deposit. function isSlashable(uint128 _id) external view returns (bool) { - DepositState memory _deposit = getDeposit(_id); + DepositState memory _deposit = getDepositState(_id); return (block.timestamp < _deposit.unlock); } diff --git a/src/examples/DataService.sol b/src/examples/DataService.sol index ce82a98..54e846e 100644 --- a/src/examples/DataService.sol +++ b/src/examples/DataService.sol @@ -43,7 +43,7 @@ contract DataService is Ownable, IDataService { /// Called by data service provider to receive payment. This locks the given deposit to begin a dispute period. function remitPayment(address _providerAddr, uint128 _depositID, uint64 _unlock) public { ProviderState memory _provider = getProviderState(_providerAddr); - Collateralization.DepositState memory _deposit = collateralization.getDeposit(_depositID); + Collateralization.DepositState memory _deposit = collateralization.getDepositState(_depositID); uint256 minCollateral = uint256(_provider.payment) * 10; require(_deposit.amount >= minCollateral, "collateral below minimum"); diff --git a/src/examples/LoanAggregator.sol b/src/examples/LoanAggregator.sol index 025ec14..3b32f12 100644 --- a/src/examples/LoanAggregator.sol +++ b/src/examples/LoanAggregator.sol @@ -57,7 +57,7 @@ contract LoanAggregator { } function withdraw(uint128 _depositID) public { - Collateralization.DepositState memory _deposit = collateralization.getDeposit(_depositID); + Collateralization.DepositState memory _deposit = collateralization.getDepositState(_depositID); collateralization.withdraw(_depositID); // calculate original deposit amount uint256 _index = 0; diff --git a/test/Collateralization.t.sol b/test/Collateralization.t.sol index 55b8e40..fa13f00 100644 --- a/test/Collateralization.t.sol +++ b/test/Collateralization.t.sol @@ -65,7 +65,7 @@ contract CollateralizationHandler is CommonBase, StdUtils { function slash(uint256 __sender, uint256 __id, uint256 __amount) public { uint128 _id = _genID(__id); vm.prank(_genActor(__sender)); - collateralization.slash(_id, bound(__amount, 0, collateralization.getDeposit(_id).amount)); + collateralization.slash(_id, bound(__amount, 0, collateralization.getDepositState(_id).amount)); assert(collateralization.isSlashable(_id)); _removeDepositID(_id); } @@ -75,7 +75,7 @@ contract CollateralizationHandler is CommonBase, StdUtils { uint64 _index = 0; while (_index < depositIDs.length) { uint128 _id = depositIDs[_index]; - Collateralization.DepositState memory _deposit = collateralization.getDeposit(_id); + Collateralization.DepositState memory _deposit = collateralization.getDepositState(_id); if (_deposit.depositor != address(0)) { total += _deposit.amount; } @@ -139,7 +139,7 @@ contract CollateralizationUnitTests is Test { uint128 _id = collateralization.deposit(address(0), 1, 0); assertEq(token.balanceOf(address(this)), _initialBalance - 1); assertEq(token.balanceOf(address(collateralization)), 1); - assertEq(collateralization.getDeposit(_id).depositor, address(this)); + assertEq(collateralization.getDepositState(_id).depositor, address(this)); assertEq(collateralization.isSlashable(_id), false); } @@ -150,7 +150,7 @@ contract CollateralizationUnitTests is Test { uint128 _id = collateralization.deposit(address(0), 1, _unlock); assertEq(token.balanceOf(address(this)), _initialBalance - 1); assertEq(token.balanceOf(address(collateralization)), 1); - assertEq(collateralization.getDeposit(_id).depositor, address(this)); + assertEq(collateralization.getDepositState(_id).depositor, address(this)); assertEq(collateralization.isSlashable(_id), true); } @@ -193,7 +193,7 @@ contract CollateralizationUnitTests is Test { } function testFail_getDepositNoDeposit() public view { - collateralization.getDeposit(0); + collateralization.getDepositState(0); } function test_Slash() public { From 7dd428ac9eb4da82d19cb12888313b372fd4d673 Mon Sep 17 00:00:00 2001 From: Theo Butler Date: Wed, 9 Aug 2023 09:19:15 -0400 Subject: [PATCH 22/23] feat: add chainid to deposit ID hash --- src/Collateralization.sol | 6 +++--- test/Collateralization.t.sol | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Collateralization.sol b/src/Collateralization.sol index a0e3cd8..4276429 100644 --- a/src/Collateralization.sol +++ b/src/Collateralization.sol @@ -74,7 +74,7 @@ contract Collateralization is Multicall { /// @return id Unique ID associated with the new deposit. function deposit(address _arbiter, uint256 _amount, uint64 _unlock) external returns (uint128) { uint64 _nonce = nonces[msg.sender]++; - uint128 _id = uint128(bytes16(keccak256(abi.encode(msg.sender, _nonce)))); + uint128 _id = uint128(bytes16(keccak256(abi.encode(block.chainid, msg.sender, _nonce)))); deposits[_id] = DepositState({ depositor: msg.sender, arbiter: _arbiter, @@ -149,7 +149,7 @@ contract Collateralization is Multicall { /// Return the next deposit ID for the given depositor address. function nextID(address _depositor) external view returns (uint128) { - uint64 _nonce = nonces[_depositor] + 1; - return uint128(bytes16(keccak256(abi.encode(msg.sender, _nonce)))); + uint64 _nonce = nonces[_depositor]; + return uint128(bytes16(keccak256(abi.encode(block.chainid, msg.sender, _nonce)))); } } diff --git a/test/Collateralization.t.sol b/test/Collateralization.t.sol index fa13f00..2456b40 100644 --- a/test/Collateralization.t.sol +++ b/test/Collateralization.t.sol @@ -136,7 +136,9 @@ contract CollateralizationUnitTests is Test { function test_UnlockedDeposit() public { uint256 _initialBalance = token.balanceOf(address(this)); token.approve(address(collateralization), 1); + uint128 _next_id = collateralization.nextID(address(this)); uint128 _id = collateralization.deposit(address(0), 1, 0); + assertEq(_id, _next_id); assertEq(token.balanceOf(address(this)), _initialBalance - 1); assertEq(token.balanceOf(address(collateralization)), 1); assertEq(collateralization.getDepositState(_id).depositor, address(this)); From 95b30ea8ee8557a076cbbc12b4f2066e09e640b9 Mon Sep 17 00:00:00 2001 From: Theo Butler Date: Wed, 9 Aug 2023 14:42:30 -0400 Subject: [PATCH 23/23] rename: Collateralization -> HorizonCore --- ...{Collateralization.sol => HorizonCore.sol} | 4 +- src/examples/DataService.sol | 18 +- src/examples/Lender.sol | 8 +- src/examples/LoanAggregator.sol | 22 +-- test/Example.t.sol | 16 +- ...lateralization.t.sol => HorizonCore.t.sol} | 174 +++++++++--------- 6 files changed, 120 insertions(+), 122 deletions(-) rename src/{Collateralization.sol => HorizonCore.sol} (98%) rename test/{Collateralization.t.sol => HorizonCore.t.sol} (55%) diff --git a/src/Collateralization.sol b/src/HorizonCore.sol similarity index 98% rename from src/Collateralization.sol rename to src/HorizonCore.sol index 4276429..56051f2 100644 --- a/src/Collateralization.sol +++ b/src/HorizonCore.sol @@ -4,8 +4,8 @@ pragma solidity ^0.8.13; import "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.sol"; import "openzeppelin-contracts/contracts/utils/Multicall.sol"; -/// This contract manages slashable, potentially time-locked token deposits. -contract Collateralization is Multicall { +/// This contract manages collateral in the form of slashable, potentially time-locked token deposits. +contract HorizonCore is Multicall { event Deposit( uint128 indexed id, address indexed depositor, address indexed arbiter, uint256 amount, uint64 unlock ); diff --git a/src/examples/DataService.sol b/src/examples/DataService.sol index 54e846e..9275073 100644 --- a/src/examples/DataService.sol +++ b/src/examples/DataService.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.13; import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol"; -import {Collateralization} from "../Collateralization.sol"; +import {HorizonCore} from "../HorizonCore.sol"; import {IDataService} from "./LoanAggregator.sol"; contract DataService is Ownable, IDataService { @@ -11,12 +11,12 @@ contract DataService is Ownable, IDataService { uint128 payment; } - Collateralization public collateralization; + HorizonCore public core; mapping(address => ProviderState) public providers; uint64 public disputePeriod; - constructor(Collateralization _collateralization, uint64 _disputePeriod) { - collateralization = _collateralization; + constructor(HorizonCore _core, uint64 _disputePeriod) { + core = _core; disputePeriod = _disputePeriod; } @@ -25,7 +25,7 @@ contract DataService is Ownable, IDataService { require(_payment > 0); require(providers[_provider].payment == 0, "provider exists"); providers[_provider] = ProviderState({deposit: 0, payment: _payment}); - collateralization.token().transferFrom(msg.sender, address(this), _payment); + core.token().transferFrom(msg.sender, address(this), _payment); } function removeProvider(address _provider) public onlyOwner { @@ -37,13 +37,13 @@ contract DataService is Ownable, IDataService { /// Slash the provider's deposit. function slash(address _provider, uint256 _amount) public onlyOwner { ProviderState memory _state = getProviderState(_provider); - collateralization.slash(_state.deposit, _amount); + core.slash(_state.deposit, _amount); } /// Called by data service provider to receive payment. This locks the given deposit to begin a dispute period. function remitPayment(address _providerAddr, uint128 _depositID, uint64 _unlock) public { ProviderState memory _provider = getProviderState(_providerAddr); - Collateralization.DepositState memory _deposit = collateralization.getDepositState(_depositID); + HorizonCore.DepositState memory _deposit = core.getDepositState(_depositID); uint256 minCollateral = uint256(_provider.payment) * 10; require(_deposit.amount >= minCollateral, "collateral below minimum"); @@ -52,9 +52,9 @@ contract DataService is Ownable, IDataService { providers[_providerAddr].deposit = _depositID; if (_deposit.unlock == 0) { - collateralization.lock(_depositID, _unlock); + core.lock(_depositID, _unlock); } - collateralization.token().transfer(_providerAddr, _provider.payment); + core.token().transfer(_providerAddr, _provider.payment); } function getProviderState(address _provider) public view returns (ProviderState memory) { diff --git a/src/examples/Lender.sol b/src/examples/Lender.sol index c52348d..824358a 100644 --- a/src/examples/Lender.sol +++ b/src/examples/Lender.sol @@ -27,7 +27,7 @@ contract Lender is Ownable, ILender { } function collect(uint256 _amount) public onlyOwner returns (bool) { - return agg.collateralization().token().transfer(owner(), _amount); + return agg.core().token().transfer(owner(), _amount); } function borrow(uint256 _amount, uint256 _collateral, uint256 _payment, uint64 _unlock) @@ -40,11 +40,11 @@ contract Lender is Ownable, ILender { require(_amount <= limits.maxAmount, "amount over maximum"); require(_payment >= expectedPayment(_amount, _duration), "payment below expected"); uint256 _transferAmount = _collateral + _payment; - agg.collateralization().token().transferFrom(msg.sender, address(this), _transferAmount); + agg.core().token().transferFrom(msg.sender, address(this), _transferAmount); uint96 _loanIndex = uint96(loans.length); loans.push(LoanState({borrower: msg.sender, initialAmount: _amount, borrowerCollateral: _collateral})); - agg.collateralization().token().approve(address(agg), _amount); + agg.core().token().approve(address(agg), _amount); return LoanCommitment({ loan: AggregatedLoan({lender: this, lenderData: _loanIndex, amount: _amount}), signature: "siggy" @@ -68,7 +68,7 @@ contract Lender is Ownable, ILender { delete loans[_lenderData]; uint256 _loss = _loan.initialAmount - _amount; if (_loss < _loan.borrowerCollateral) { - agg.collateralization().token().transfer(_loan.borrower, _loan.borrowerCollateral - _loss); + agg.core().token().transfer(_loan.borrower, _loan.borrowerCollateral - _loss); } } } diff --git a/src/examples/LoanAggregator.sol b/src/examples/LoanAggregator.sol index 3b32f12..a8fa719 100644 --- a/src/examples/LoanAggregator.sol +++ b/src/examples/LoanAggregator.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.13; -import {Collateralization} from "../Collateralization.sol"; +import {HorizonCore} from "../HorizonCore.sol"; interface IDataService { function remitPayment(address _provider, uint128 _deposit, uint64 _unlock) external; @@ -23,11 +23,11 @@ struct AggregatedLoan { } contract LoanAggregator { - Collateralization public collateralization; + HorizonCore public core; mapping(uint128 => AggregatedLoan[]) public loans; - constructor(Collateralization _collateralization) { - collateralization = _collateralization; + constructor(HorizonCore _core) { + core = _core; } function remitPayment(IDataService _arbiter, uint64 _unlock, LoanCommitment[] calldata _loanCommitments) @@ -40,13 +40,11 @@ contract LoanAggregator { LoanCommitment memory _commitment = _loanCommitments[_index]; // TODO: verify signature of (lender, amount, arbiter, unlock) _amount += _commitment.loan.amount; - collateralization.token().transferFrom( - address(_commitment.loan.lender), address(this), _commitment.loan.amount - ); + core.token().transferFrom(address(_commitment.loan.lender), address(this), _commitment.loan.amount); _index += 1; } - collateralization.token().approve(address(collateralization), _amount); - uint128 _deposit = collateralization.deposit(address(_arbiter), _amount, _unlock); + core.token().approve(address(core), _amount); + uint128 _deposit = core.deposit(address(_arbiter), _amount, _unlock); _index = 0; while (_index < _loanCommitments.length) { loans[_deposit].push(_loanCommitments[_index].loan); @@ -57,8 +55,8 @@ contract LoanAggregator { } function withdraw(uint128 _depositID) public { - Collateralization.DepositState memory _deposit = collateralization.getDepositState(_depositID); - collateralization.withdraw(_depositID); + HorizonCore.DepositState memory _deposit = core.getDepositState(_depositID); + core.withdraw(_depositID); // calculate original deposit amount uint256 _index = 0; uint256 _initialAmount = 0; @@ -71,7 +69,7 @@ contract LoanAggregator { while (_index < loans[_depositID].length) { AggregatedLoan memory _loan = loans[_depositID][_index]; uint256 _lenderReturn = (_loan.amount * _deposit.amount) / _initialAmount; - collateralization.token().transfer(address(_loan.lender), _lenderReturn); + core.token().transfer(address(_loan.lender), _lenderReturn); _loan.lender.onCollateralWithraw(_lenderReturn, _loan.lenderData); _index += 1; } diff --git a/test/Example.t.sol b/test/Example.t.sol index 62644e7..98d7e63 100644 --- a/test/Example.t.sol +++ b/test/Example.t.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.13; import {Test} from "forge-std/Test.sol"; import {ERC20Burnable} from "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.sol"; import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; -import {Collateralization} from "../src/Collateralization.sol"; +import {HorizonCore} from "../src/HorizonCore.sol"; import {DataService} from "../src/examples/DataService.sol"; import {Lender, Limits} from "../src/examples/Lender.sol"; import { @@ -17,9 +17,9 @@ contract TestToken is ERC20Burnable { } } -contract CollateralizationUnitTests is Test, ILender { +contract HorizonCoreUnitTests is Test, ILender { TestToken public token; - Collateralization public collateralization; + HorizonCore public core; DataService public dataService; LoanAggregator public aggregator; Lender public lender; @@ -28,9 +28,9 @@ contract CollateralizationUnitTests is Test, ILender { function setUp() public { token = new TestToken(1_000); - collateralization = new Collateralization(token); - aggregator = new LoanAggregator(collateralization); - dataService = new DataService(collateralization, 20 days); + core = new HorizonCore(token); + aggregator = new LoanAggregator(core); + dataService = new DataService(core, 20 days); lender = new Lender(aggregator, Limits({maxAmount: 100, maxDuration: 30 days})); token.transfer(address(lender), 80); } @@ -43,7 +43,7 @@ contract CollateralizationUnitTests is Test, ILender { uint256 _initialBalance = token.balanceOf(address(this)); uint256 _initialLenderBalance = token.balanceOf(address(lender)); - // Data service requires 10x of payment (100 tokens) in collateralization deposit. Fund deposit with a 20 token + // Data service requires 10x of payment (100 tokens) in a HorizonCore deposit. Fund deposit with a 20 token // loan from self, and a 80 token loan from lender. LoanCommitment[] memory _loanCommitments = new LoanCommitment[](2); token.approve(address(aggregator), 20); @@ -74,7 +74,7 @@ contract CollateralizationUnitTests is Test, ILender { uint256 _initialBalance = token.balanceOf(address(this)); uint256 _initialLenderBalance = token.balanceOf(address(lender)); - // Data service requires 10x of payment (100 tokens) in collateralization deposit. Fund deposit with a 20 token + // Data service requires 10x of payment (100 tokens) in a HorizonCore deposit. Fund deposit with a 20 token // loan from self, and a 80 token loan from lender. LoanCommitment[] memory _loanCommitments = new LoanCommitment[](2); token.approve(address(aggregator), 20); diff --git a/test/Collateralization.t.sol b/test/HorizonCore.t.sol similarity index 55% rename from test/Collateralization.t.sol rename to test/HorizonCore.t.sol index 2456b40..d311dac 100644 --- a/test/Collateralization.t.sol +++ b/test/HorizonCore.t.sol @@ -6,7 +6,7 @@ import {StdUtils} from "forge-std/StdUtils.sol"; import {Test} from "forge-std/Test.sol"; import {ERC20Burnable} from "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.sol"; import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; -import {Collateralization} from "../src/Collateralization.sol"; +import {HorizonCore} from "../src/HorizonCore.sol"; contract TestToken is ERC20Burnable { constructor(uint256 _initialSupply) ERC20("MockCoin", "MOCK") { @@ -14,20 +14,20 @@ contract TestToken is ERC20Burnable { } } -contract CollateralizationHandler is CommonBase, StdUtils { - Collateralization public collateralization; +contract HorizonCoreHandler is CommonBase, StdUtils { + HorizonCore public core; uint256 tokenSupply; uint128[] depositIDs; address[] actors; constructor() { tokenSupply = 900; - collateralization = new Collateralization(new TestToken(tokenSupply)); + core = new HorizonCore(new TestToken(tokenSupply)); actors = [address(1), address(2), address(3)]; uint256 _index = 0; while (_index < actors.length) { - collateralization.token().transfer(actors[_index], tokenSupply / (actors.length)); + core.token().transfer(actors[_index], tokenSupply / (actors.length)); _index += 1; } } @@ -41,10 +41,10 @@ contract CollateralizationHandler is CommonBase, StdUtils { returns (uint128) { address _depositor = _genActor(__sender); - uint256 _amount = bound(__amount, 1, collateralization.token().balanceOf(_depositor)); + uint256 _amount = bound(__amount, 1, core.token().balanceOf(_depositor)); vm.startPrank(_depositor); - collateralization.token().approve(address(collateralization), _amount); - uint128 _id = collateralization.deposit(_genActor(__arbiter), _amount, _genTimestamp(__unlock)); + core.token().approve(address(core), _amount); + uint128 _id = core.deposit(_genActor(__arbiter), _amount, _genTimestamp(__unlock)); vm.stopPrank(); depositIDs.push(_id); return _id; @@ -52,21 +52,21 @@ contract CollateralizationHandler is CommonBase, StdUtils { function lock(uint256 __sender, uint256 __id, uint256 __unlock) public { vm.prank(_genActor(__sender)); - collateralization.lock(_genID(__id), _genTimestamp(__unlock)); + core.lock(_genID(__id), _genTimestamp(__unlock)); } function withdraw(uint256 __sender, uint256 __id) public { uint128 _id = _genID(__id); vm.prank(_genActor(__sender)); - collateralization.withdraw(_id); + core.withdraw(_id); _removeDepositID(_id); } function slash(uint256 __sender, uint256 __id, uint256 __amount) public { uint128 _id = _genID(__id); vm.prank(_genActor(__sender)); - collateralization.slash(_id, bound(__amount, 0, collateralization.getDepositState(_id).amount)); - assert(collateralization.isSlashable(_id)); + core.slash(_id, bound(__amount, 0, core.getDepositState(_id).amount)); + assert(core.isSlashable(_id)); _removeDepositID(_id); } @@ -75,7 +75,7 @@ contract CollateralizationHandler is CommonBase, StdUtils { uint64 _index = 0; while (_index < depositIDs.length) { uint128 _id = depositIDs[_index]; - Collateralization.DepositState memory _deposit = collateralization.getDepositState(_id); + HorizonCore.DepositState memory _deposit = core.getDepositState(_id); if (_deposit.depositor != address(0)) { total += _deposit.amount; } @@ -109,199 +109,199 @@ contract CollateralizationHandler is CommonBase, StdUtils { } } -contract CollateralizationInvariants is Test { - CollateralizationHandler public handler; +contract HorizonCoreInvariants is Test { + HorizonCoreHandler public handler; ERC20Burnable public token; function setUp() public { - handler = new CollateralizationHandler(); - token = handler.collateralization().token(); + handler = new HorizonCoreHandler(); + token = handler.core().token(); targetContract(address(handler)); } function invariant_depositBalance() public { - assertEq(token.balanceOf(address(handler.collateralization())), handler.depositTotal()); + assertEq(token.balanceOf(address(handler.core())), handler.depositTotal()); } } -contract CollateralizationUnitTests is Test { +contract HorizonCoreUnitTests is Test { TestToken public token; - Collateralization public collateralization; + HorizonCore public core; function setUp() public { token = new TestToken(1_000); - collateralization = new Collateralization(token); + core = new HorizonCore(token); } function test_UnlockedDeposit() public { uint256 _initialBalance = token.balanceOf(address(this)); - token.approve(address(collateralization), 1); - uint128 _next_id = collateralization.nextID(address(this)); - uint128 _id = collateralization.deposit(address(0), 1, 0); + token.approve(address(core), 1); + uint128 _next_id = core.nextID(address(this)); + uint128 _id = core.deposit(address(0), 1, 0); assertEq(_id, _next_id); assertEq(token.balanceOf(address(this)), _initialBalance - 1); - assertEq(token.balanceOf(address(collateralization)), 1); - assertEq(collateralization.getDepositState(_id).depositor, address(this)); - assertEq(collateralization.isSlashable(_id), false); + assertEq(token.balanceOf(address(core)), 1); + assertEq(core.getDepositState(_id).depositor, address(this)); + assertEq(core.isSlashable(_id), false); } function test_LockedDeposit() public { uint64 _unlock = uint64(block.timestamp) + 1; uint256 _initialBalance = token.balanceOf(address(this)); - token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(address(0), 1, _unlock); + token.approve(address(core), 1); + uint128 _id = core.deposit(address(0), 1, _unlock); assertEq(token.balanceOf(address(this)), _initialBalance - 1); - assertEq(token.balanceOf(address(collateralization)), 1); - assertEq(collateralization.getDepositState(_id).depositor, address(this)); - assertEq(collateralization.isSlashable(_id), true); + assertEq(token.balanceOf(address(core)), 1); + assertEq(core.getDepositState(_id).depositor, address(this)); + assertEq(core.isSlashable(_id), true); } function test_DepositUniqueID() public { - token.approve(address(collateralization), 2); - uint128 _id1 = collateralization.deposit(address(0), 1, 0); - uint128 _id2 = collateralization.deposit(address(0), 1, 0); + token.approve(address(core), 2); + uint128 _id1 = core.deposit(address(0), 1, 0); + uint128 _id2 = core.deposit(address(0), 1, 0); assertNotEq(_id1, _id2); } function test_Lock() public { uint64 _unlock = uint64(block.timestamp) + 1; - token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(address(this), 1, 0); - assertEq(collateralization.isSlashable(_id), false); - collateralization.lock(_id, _unlock); - assertEq(collateralization.isSlashable(_id), true); + token.approve(address(core), 1); + uint128 _id = core.deposit(address(this), 1, 0); + assertEq(core.isSlashable(_id), false); + core.lock(_id, _unlock); + assertEq(core.isSlashable(_id), true); } function testFail_LockLocked() public { uint64 _unlock = uint64(block.timestamp) + 1; - token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(address(this), 1, _unlock); - collateralization.lock(_id, _unlock); + token.approve(address(core), 1); + uint128 _id = core.deposit(address(this), 1, _unlock); + core.lock(_id, _unlock); } function testFail_LockLockedModify() public { uint64 _unlock = uint64(block.timestamp) + 1; - token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(address(this), 1, _unlock); - collateralization.lock(_id, _unlock - 1); + token.approve(address(core), 1); + uint128 _id = core.deposit(address(this), 1, _unlock); + core.lock(_id, _unlock - 1); } function testFail_LockAfterUnlock() public { uint64 _unlock = uint64(block.timestamp) + 1; - token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(address(this), 1, _unlock); + token.approve(address(core), 1); + uint128 _id = core.deposit(address(this), 1, _unlock); vm.warp(_unlock + 1); - collateralization.lock(_id, _unlock + 1); + core.lock(_id, _unlock + 1); } function testFail_getDepositNoDeposit() public view { - collateralization.getDepositState(0); + core.getDepositState(0); } function test_Slash() public { uint64 _unlock = uint64(block.timestamp) + 3; address _arbiter = 0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF; uint256 _initialSupply = token.totalSupply(); - token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(_arbiter, 1, _unlock); + token.approve(address(core), 1); + uint128 _id = core.deposit(_arbiter, 1, _unlock); vm.warp(_unlock - 1); vm.prank(_arbiter); - collateralization.slash(_id, 1); + core.slash(_id, 1); assertEq(token.totalSupply(), _initialSupply - 1); } function testFail_SlashAtUnlock() public { uint64 _unlock = uint64(block.timestamp) + 3; - token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(address(this), 1, _unlock); + token.approve(address(core), 1); + uint128 _id = core.deposit(address(this), 1, _unlock); vm.warp(_unlock); - collateralization.slash(_id, 1); + core.slash(_id, 1); } function testFail_SlashAfterUnlock() public { uint64 _unlock = uint64(block.timestamp) + 3; - token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(address(this), 1, _unlock); + token.approve(address(core), 1); + uint128 _id = core.deposit(address(this), 1, _unlock); vm.warp(_unlock + 1); - collateralization.slash(_id, 1); + core.slash(_id, 1); } function testFail_SlashUnlocked() public { uint64 _unlock = uint64(block.timestamp) + 3; - token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(address(this), 1, _unlock); + token.approve(address(core), 1); + uint128 _id = core.deposit(address(this), 1, _unlock); vm.warp(_unlock + 1); - collateralization.slash(_id, 1); + core.slash(_id, 1); } function testFail_SlashFromNonArbiter() public { uint64 _unlock = uint64(block.timestamp) + 3; address _arbiter = 0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF; - token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(_arbiter, 1, _unlock); + token.approve(address(core), 1); + uint128 _id = core.deposit(_arbiter, 1, _unlock); vm.warp(_unlock - 1); - collateralization.slash(_id, 1); + core.slash(_id, 1); } function test_WithdrawAtUnlock() public { uint64 _unlock = uint64(block.timestamp) + 1; uint256 _initialBalance = token.balanceOf(address(this)); - token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(address(this), 1, _unlock); + token.approve(address(core), 1); + uint128 _id = core.deposit(address(this), 1, _unlock); vm.warp(_unlock); - collateralization.withdraw(_id); + core.withdraw(_id); assertEq(token.balanceOf(address(this)), _initialBalance); - assertEq(token.balanceOf(address(collateralization)), 0); + assertEq(token.balanceOf(address(core)), 0); } function test_WithdrawAfterUnlock() public { uint64 _unlock = uint64(block.timestamp) + 1; uint256 _initialBalance = token.balanceOf(address(this)); - token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(address(this), 1, _unlock); + token.approve(address(core), 1); + uint128 _id = core.deposit(address(this), 1, _unlock); vm.warp(_unlock + 1); - collateralization.withdraw(_id); + core.withdraw(_id); assertEq(token.balanceOf(address(this)), _initialBalance); - assertEq(token.balanceOf(address(collateralization)), 0); + assertEq(token.balanceOf(address(core)), 0); } function testFail_WithdrawBeforeUnlock() public { - token.approve(address(collateralization), 1); + token.approve(address(core), 1); uint64 _unlock = uint64(block.timestamp) + 3; - uint128 _id = collateralization.deposit(address(this), 1, _unlock); + uint128 _id = core.deposit(address(this), 1, _unlock); vm.warp(_unlock - 1); - collateralization.withdraw(_id); + core.withdraw(_id); } function test_WithdrawLocked() public { uint64 _unlock = uint64(block.timestamp) + 1; uint256 _initialBalance = token.balanceOf(address(this)); - token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(address(this), 1, _unlock); + token.approve(address(core), 1); + uint128 _id = core.deposit(address(this), 1, _unlock); vm.warp(_unlock); - collateralization.withdraw(_id); + core.withdraw(_id); assertEq(token.balanceOf(address(this)), _initialBalance); - assertEq(token.balanceOf(address(collateralization)), 0); + assertEq(token.balanceOf(address(core)), 0); } function testFail_WithdrawTwice() public { uint64 _unlock = uint64(block.timestamp) + 1; - token.approve(address(collateralization), 2); - uint128 _id = collateralization.deposit(address(this), 2, _unlock); + token.approve(address(core), 2); + uint128 _id = core.deposit(address(this), 2, _unlock); vm.warp(_unlock); - collateralization.withdraw(_id); - collateralization.withdraw(_id); + core.withdraw(_id); + core.withdraw(_id); } function testFail_WithdrawFromNonDepositor() public { uint64 _unlock = uint64(block.timestamp) + 1; uint256 _initialBalance = token.balanceOf(address(this)); address _other = 0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF; - token.approve(address(collateralization), 1); - uint128 _id = collateralization.deposit(address(this), 1, _unlock); + token.approve(address(core), 1); + uint128 _id = core.deposit(address(this), 1, _unlock); vm.warp(_unlock); vm.prank(_other); - collateralization.withdraw(_id); + core.withdraw(_id); assertEq(token.balanceOf(address(this)), _initialBalance); } }