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);