Skip to content

Commit

Permalink
Support unlocked deposits
Browse files Browse the repository at this point in the history
  • Loading branch information
Theodus committed Jul 24, 2023
1 parent f621d5d commit 5994d51
Show file tree
Hide file tree
Showing 6 changed files with 219 additions and 271 deletions.
209 changes: 91 additions & 118 deletions src/Collateralization.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);
}
}
14 changes: 7 additions & 7 deletions src/examples/DataService.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
}
Expand All @@ -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);
}

Expand Down
4 changes: 2 additions & 2 deletions src/examples/Lender.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
14 changes: 7 additions & 7 deletions src/examples/LoanAggregator.sol
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -30,34 +30,34 @@ 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)
{
uint256 _index = 0;
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
);
_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;
Expand Down
Loading

0 comments on commit 5994d51

Please sign in to comment.