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..707599d 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 { @@ -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..512a0b3 100644 --- a/src/examples/Lender.sol +++ b/src/examples/Lender.sol @@ -1,74 +1,74 @@ // 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, ILender, LoanAggregator, LoanCommitment} from "./LoanAggregator.sol"; +// import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol"; +// import {SafeCast} from "openzeppelin-contracts/contracts/utils/math/SafeCast.sol"; +// import {AggregatedLoan, ILender, LoanAggregator, LoanCommitment} from "./LoanAggregator.sol"; -struct Limits { - uint256 maxValue; - uint64 maxDuration; -} +// struct Limits { +// uint256 maxValue; +// uint64 maxDuration; +// } -contract Lender is Ownable, ILender { - struct LoanState { - address borrower; - uint256 initialValue; - uint256 borrowerCollateral; - } +// contract Lender is Ownable, ILender { +// struct LoanState { +// address borrower; +// uint256 initialValue; +// uint256 borrowerCollateral; +// } - LoanAggregator public agg; - Limits public limits; - LoanState[] public loans; +// LoanAggregator public agg; +// Limits public limits; +// LoanState[] public loans; - constructor(LoanAggregator _agg, Limits memory _limits) { - agg = _agg; - limits = _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 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); +// 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); - 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: this, lenderData: _loanIndex, value: _value}), - signature: "siggy" - }); - } +// 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: this, lenderData: _loanIndex, value: _value}), +// 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; - } +// /// 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; +// } - 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); - } - } -} +// 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 7d30d09..eb88638 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) { @@ -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..1c0eb70 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,7 +36,7 @@ contract CollateralizationHandler is CommonBase, StdUtils { vm.warp(block.timestamp + bound(blocks, 1, 10)); } - function deposit(uint256 __sender, uint256 __value, uint256 __expiration, uint256 __arbiter) + function deposit(uint256 __sender, uint256 __arbiter, uint256 __value, uint256 __unlock) public returns (uint128) { @@ -44,15 +44,15 @@ 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(_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 +75,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 +104,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 +133,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..7242f18 100644 --- a/test/Example.t.sol +++ b/test/Example.t.sol @@ -1,102 +1,102 @@ // 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, ILender, LoanAggregator, LoanCommitment -} from "../src/examples/LoanAggregator.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 {DataService} from "../src/examples/DataService.sol"; +// import {Lender, Limits} from "../src/examples/Lender.sol"; +// import { +// AggregatedLoan, IDataService, ILender, LoanAggregator, LoanCommitment +// } from "../src/examples/LoanAggregator.sol"; -contract TestToken is ERC20Burnable { - constructor(uint256 _initialSupply) ERC20("MockCoin", "MOCK") { - _mint(msg.sender, _initialSupply); - } -} +// contract TestToken is ERC20Burnable { +// constructor(uint256 _initialSupply) ERC20("MockCoin", "MOCK") { +// _mint(msg.sender, _initialSupply); +// } +// } -contract CollateralizationUnitTests is Test, ILender { - TestToken public token; - Collateralization public collateralization; - DataService public dataService; - LoanAggregator public aggregator; - Lender public lender; +// 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 onCollateralWithraw(uint256 _value, uint96 _lenderData) public {} - function onCollateralWithraw(uint256 _value, 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})); +// token.transfer(address(lender), 80); +// } - 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); +// 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 _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 - // loan from self, and a 80 token loan from lender. - LoanCommitment[] memory _loanCommitments = new LoanCommitment[](2); - 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); +// // 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(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 + 6 - 80); +// assertEq(token.balanceOf(address(this)), _initialBalance + 10 - 26); +// 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); - } +// // 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); +// } - function test_ExampleSlash() 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); +// function test_ExampleSlash() 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 _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 - // loan from self, and a 80 token loan from lender. - LoanCommitment[] memory _loanCommitments = new LoanCommitment[](2); - 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); +// // 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(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 + 6 - 80); +// assertEq(token.balanceOf(address(this)), _initialBalance + 10 - 26); +// 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); - } -} +// // 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); +// } +// }