mirror of
https://github.com/logos-storage/logos-storage-research.git
synced 2026-01-04 14:33:09 +00:00
deployment: vault contract updates (#199)
Updates to the API of the Vault contract as worked on in codex-storage/codex-contracts-eth#220 Most important changes: - token transfers are much more restricted in the vault, improving its safety - when a storage request fails, only the providers at fault are punished, the rest are not - when a storage request fails, the client is only reimbursed for the remaining time in the contract - no longer possible for the marketplace to request one final storage proof before allowing withdrawal - repair reward is temporarily stored with the client's funds while slot is free, and not yet filled; this could lead to client withdrawing repair rewards when slots are free when the request ends
This commit is contained in:
parent
e99af7d03e
commit
4a5ddf0e52
@ -29,19 +29,19 @@ Requests on the new smart contract, but it will still fulfill the duties of the
|
||||
contract.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────
|
||||
│ Contract v2
|
||||
┌───────────────────────────────┼────────────────────┬──────────────────────►
|
||||
┌─────────────────────────────────────────
|
||||
│ Contract v2
|
||||
┌───────────────────────────────┼────────────────────┬──────────────────────►
|
||||
│ Contract v1 │┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼│ time
|
||||
└───────────────────────────────┼────────────────────┤
|
||||
│ Grace period │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
Codex release with Codex release that
|
||||
new smart contract removes old contract
|
||||
└───────────────────────────────┼────────────────────┤
|
||||
│ Grace period │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
Codex release with Codex release that
|
||||
new smart contract removes old contract
|
||||
```
|
||||
|
||||
An important consideration for this upgrade mechanism is that we plan to limit the maximum duration of Storage Requests.
|
||||
@ -102,7 +102,7 @@ initial ideals of a permissionless approach.
|
||||
### Upgradable contract
|
||||
|
||||
OpenZeppelin has tooling for writing [Upgradable contracts](https://docs.openzeppelin.com/upgrades-plugins/1.x/). While
|
||||
this approach has some technical limitations, it should be sufficient for realizing bug fixes as part of emergency upgrades.
|
||||
this approach has some technical limitations, it should be sufficient for realizing bug fixes as part of emergency upgrades.
|
||||
This would require an admin role that would have this capability.
|
||||
|
||||
While this solution provides the most capable and flexible way to ensure security, it also gives "too much power" for
|
||||
@ -191,71 +191,128 @@ contracts — a business logic contract and a funds vault contract.
|
||||
We will reference the business logic contract as **Marketplace contract** as it will contain the Marketplace logic.
|
||||
It will be possible to perform emergency upgrades using concepts described in the [Upgradable contract](#upgradable-contract) section.
|
||||
In this way, if there is a bug/exploit that would affect the funds, it is possible to patch it quickly.
|
||||
The original "feature upgrade" path still holds with this approach, where this business logic contract would get upgraded
|
||||
The original "feature upgrade" path still holds with this approach, where this business logic contract would get upgraded
|
||||
as discussed in the [Feature upgrades](#feature-upgrades) section.
|
||||
The admin role would belong to multisig and be maintained inside the organization. This contract would not hold any funds as
|
||||
it would delegate them to the Vault contract.
|
||||
|
||||
Vault contract will have the responsibility to keep users' funds safe. It should have minimal logic incorporated to minimize
|
||||
the attack surface. Ideally, the Vault contract will be deployed once and never altered as it will have one simple task
|
||||
the attack surface. Ideally, the Vault contract will be deployed once and never altered as it will have one simple task
|
||||
to do, and there should not be a need to extend it. This contract will not incorporate the upgradability capabilities,
|
||||
but as a safety measure, it will be possible to freeze it as described in the [Freezing contract](#freezing-contract) section.
|
||||
but as a safety measure, it will be possible to freeze it as described in the [Freezing contract](#freezing-contract) section.
|
||||
Freezing the Vault would be done in case of severe exploitable bugs.
|
||||
|
||||
Thanks to this splitting of the contract, we will limit the liabilities over funds control as described
|
||||
in the [Upgradable contract](#upgradable-contract) section, but at the same it gives us the flexibility to react to
|
||||
Thanks to this splitting of the contract, we will limit the liabilities over funds control as described
|
||||
in the [Upgradable contract](#upgradable-contract) section, but at the same it gives us the flexibility to react to
|
||||
unforeseen situations.
|
||||
|
||||
The Vault contract should have logic that prevents the simultaneous draining of all the funds. We came up with two designs for
|
||||
this - time-based locking and recipient-based locking. The time-based locking Vault is described in depth below.
|
||||
The recipient-base Vault works with a locking schema where the funds have a predefined set of recipients to which the funds
|
||||
can be transferred. In this way, the hacker can't redirect the funds to their controlled accounts.
|
||||
Unfortunately, this concept is not applicable to Marketplace because of slot repairs, when one slot's host is replaced
|
||||
with another, which would require reallocating funds and hence open an opportunity for hackers to redirect the funds to
|
||||
their accounts.
|
||||
The Vault contract should have logic that prevents the simultaneous draining of all the funds. We came up with two ideas for
|
||||
this - time-based locking and recipient-based locking. The Vault described below utilizes both ideas.
|
||||
|
||||
### Time-based Vault
|
||||
### Vault
|
||||
|
||||
This Vault works on locking the funds until a certain time threshold is reached when it allows them to be spent. In this way
|
||||
there is only a tiny fraction of the funds possible to be spent at a given time by the "business logic" contract as
|
||||
we assume nodes will proactively and quickly collect their funds when able. If there is an exploit on the business logic
|
||||
This Vault works on locking the funds until a certain time threshold is reached when it allows them to be withdrawn. In this way
|
||||
there is only a tiny fraction of the funds possible to be redirected at a given time by the "business logic" contract as
|
||||
the vault only allows manipulation of funds while they are time-locked. If there is an exploit on the business logic
|
||||
contract, the attacker could withdraw only a small amount of funds.
|
||||
|
||||
We envision the following API of the Vault contract:
|
||||
|
||||
```solidity
|
||||
contract TimeVault {
|
||||
/// Creates new deposit with the given amount transferred from account "fromAccount" and locks it till spendable_from_timestamp
|
||||
function deposit(uint256 amount, addr fromAccount, uint256 spendable_from_timestamp) returns (DepositId)
|
||||
/// Deposits more funds to the already existing deposit
|
||||
function deposit(uint256 amount, addr fromAccount, uint256 spendable_from_timestamp, DepositId id) returns (DepositId)
|
||||
|
||||
/// Extends the timelock of the specified deposit
|
||||
function extend(DepositId id, uint256 spendable_from_timestamp)
|
||||
|
||||
/// Lower deposit amount of funds from the specified deposit
|
||||
function burn(DepositId id, uint256 amount)
|
||||
|
||||
/// Transfer the given amount to the recipient, provided the block's timestamp is after the deposit's time lock
|
||||
function spend(DepositId id, addr recipient, uint256 amount)
|
||||
contract Vault {
|
||||
/// Locks the fund until the expiry timestamp. The lock expiry can be extended
|
||||
/// later, but no more than the maximum timestamp.
|
||||
function lock(FundId fundId, Timestamp expiry, Timestamp maximum) {}
|
||||
|
||||
/// Deposits an amount of tokens into the vault, and adds them to the balance
|
||||
/// of the account. ERC20 tokens are transfered from the caller to the vault
|
||||
/// contract.
|
||||
function deposit(FundId fundId, AccountId accountId, uint128 amount) {}
|
||||
|
||||
/// Takes an amount of tokens from the account balance and designates them
|
||||
/// for the account holder. These tokens are no longer available to be
|
||||
/// transfered to other accounts.
|
||||
function designate(FundId fundId, AccountId accountId, uint128 amount) {}
|
||||
|
||||
/// Transfers an amount of tokens from one acount to the other.
|
||||
function transfer(FundId fundId, AccountId from, AccountId to, uint128 amount) {}
|
||||
|
||||
/// Transfers tokens from one account to the other over time.
|
||||
function flow(FundId fundId, AccountId from, AccountId to, TokensPerSecond rate) {}
|
||||
|
||||
/// Delays unlocking of a locked fund.
|
||||
function extendLock(FundId fundId, Timestamp expiry) {}
|
||||
|
||||
/// Burns an amount of designated tokens from the account.
|
||||
function burnDesignated(FundId fundId, AccountId accountId, uint128 amount) {}
|
||||
|
||||
/// Burns all tokens from the account.
|
||||
function burnAccount(FundId fundId, AccountId accountId) {}
|
||||
|
||||
/// Freezes a fund. Stops all tokens flows and disallows any operations on the
|
||||
/// fund until it unlocks.
|
||||
function freezeFund(FundId fundId) {}
|
||||
|
||||
/// Transfers all ERC20 tokens in the account out of the vault to the account
|
||||
/// holder.
|
||||
function withdraw(FundId fundId, AccountId accountId) {}
|
||||
|
||||
/// Allows an account holder to withdraw its tokens from a fund directly,
|
||||
/// bypassing the need to ask the controller of the fund to initiate the
|
||||
/// withdrawal.
|
||||
function withdrawByRecipient(Controller controller, FundId fundId, AccountId accountId) {}
|
||||
}
|
||||
```
|
||||
|
||||
_Important note is that this is a standalone contract, and it needs to keep track of the "owner of the deposit".
|
||||
Hence, only the address that performs initial deposit can manipulate the funds later on._
|
||||
_Important note is that this is a standalone contract, and it needs to keep track of the "controller of the fund".
|
||||
Hence, the funds for each controller are independent, and each controller can only manipulate its own funds._
|
||||
|
||||
Integration into the Marketplace contract would be in the following way:
|
||||
|
||||
- `deposit()` reward funds upon `requestStorage()` call with timelock until the Request's `expiry`
|
||||
- If the Request starts, the timelock is extended till the Request's end
|
||||
- If the Request expires, then funds (partial payouts for hosts and remaining funds for a client) can be withdrawn when requested
|
||||
- `deposit(depositId)` collateral funds to existing Request's deposit upon `fillSlot()`
|
||||
- If the Request expires, the collateral can be withdrawn together with partial payouts
|
||||
- Upon slot's being freed because of the host being kicked out, then `burn()` host's collateral lowered by the amount dedicated repair reward
|
||||
- Upon Request's end, collateral and rewards can be `spend()`
|
||||
- Upon Request's failure, all host's collateral is `burn()`. The original reward can be `spend()` back to the client, but only after the Request's end
|
||||
- `RequestId`s are used as `FundId` identifiers in the vault.
|
||||
- When storage is requested by a client, it leads to the following calls on the
|
||||
vault:
|
||||
- `lock(requestId, request.expiry, request.end)`
|
||||
- `deposit(requestId, clientAccount, request.price)`
|
||||
- When a slot is filled by a provider, the associated collateral is deposited
|
||||
and designated, and some of the client tokens flow to the provider:
|
||||
- `deposit(requestId, providerAccount, collateral)`
|
||||
- `designate(requestId, providerAccount, collateral - repairReward)`
|
||||
- `flow(requestId, clientAccount, providerAccount, pricePerSlotPerSecond)`
|
||||
- When a request starts, the time lock is extended:
|
||||
- `extendLock(requestId, request.end)`
|
||||
- When a request ends, then the vault will allow hosts and client to withdraw
|
||||
their tokens. This consists of collateral and partial payouts for hosts and
|
||||
any remaining funds for the client:
|
||||
- either: `withdraw(requestId, account)`
|
||||
- or: `withdrawByRecipient(marketplace, requestId, account)`
|
||||
- When a provider misses a storage proof, then a part of its collateral is
|
||||
burned:
|
||||
- `burnDesignated(requestId, providerAccount, slashAmount)`
|
||||
- When a slot is freed because a provider missed too many proofs, then the
|
||||
repair reward is set aside, the flow of tokens to the provider is reversed,
|
||||
and the rest of the provider tokens are burned:
|
||||
- `transfer(requestId, providerAccount, clientAccount, repairReward)`
|
||||
- `flow(requestId, providerAccount, clientAccount, pricePerSlotPerSecond)`
|
||||
- `burnAccount(requestId, providerAccount)`
|
||||
- When a slot is repaired then the repair reward is transfered to the new
|
||||
provider:
|
||||
- `transfer(requestId, clientAccount, providerAccount, repairReward)`
|
||||
- When a request fails, the fund is frozen:
|
||||
- `freezeFund(requestId)`
|
||||
|
||||
The main disadvantage of this approach is that when the Request fails, the client can collect its original funds only after the Request's end.
|
||||
The main downsides of this approach are:
|
||||
- it is no longer possible to burn the funds of all providers when a storage
|
||||
request fails; only the funds of providers that failed to provide storage
|
||||
proofs are burned
|
||||
- it is no longer possible to return all funds to the client when a storage
|
||||
request fails; a client is only refunded for the remaining time
|
||||
- when a request ends, then everyone can withdraw directly from the vault,
|
||||
so it's not possible for the marketplace to e.g. request one final storage
|
||||
proof before allowing withdrawal
|
||||
|
||||
We believe that the added safety of using a time vault is important enough to
|
||||
accept these downsides.
|
||||
|
||||
### Contract's architecture
|
||||
|
||||
@ -265,11 +322,11 @@ Codex node will not have to be bothered about Vault's existence as every fund's
|
||||
through the Marketplace contract. The Codex node will have a hardcoded Marketplace contract address with which it will interact.
|
||||
|
||||
```
|
||||
┌────────────┐ ┌──────────────────────┐ ┌────────────────┐
|
||||
│ │ │ │ │ │
|
||||
│ Codex Node ├────►│ Marketplace contract ├────►│ Vault contract │
|
||||
│ │ │ │ │ │
|
||||
└────────────┘ └──────────────────────┘ └────────────────┘
|
||||
┌────────────┐ ┌──────────────────────┐ ┌────────────────┐
|
||||
│ │ │ │ │ │
|
||||
│ Codex Node ├────►│ Marketplace contract ├────►│ Vault contract │
|
||||
│ │ │ │ │ │
|
||||
└────────────┘ └──────────────────────┘ └────────────────┘
|
||||
```
|
||||
|
||||
### Scenarios handling
|
||||
@ -305,10 +362,9 @@ in the [Freezing contract](#freezing-contract) section.
|
||||
|
||||
> [!CAUTION]
|
||||
> The multisig account will not have direct control over the funds as they will be in the safekeeping of the Vault.
|
||||
> But with the current Vault design, only the depositor can manipulate the funds, which in our case
|
||||
> is the Marketplace contract that the multisig has control over. Maybe we should consider how users could
|
||||
> withdraw funds from the Vault directly without interacting through the Marketplace contract. That will have to be
|
||||
> thoroughly considered, as allowing such a thing could hinder the security guarantees of the Vault.
|
||||
> But with the current Vault design, only the controller can manipulate the funds, which in our case
|
||||
> is the Marketplace contract that the multisig has control over. This is why users can withdraw funds from the
|
||||
> Vault directly without interacting through the Marketplace contract.
|
||||
|
||||
#### Vault emergency
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user