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:
markspanbroek 2025-03-10 10:26:15 +01:00 committed by GitHub
parent e99af7d03e
commit 4a5ddf0e52
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -206,56 +206,113 @@ Thanks to this splitting of the contract, we will limit the liabilities over fun
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)
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) {}
/// Extends the timelock of the specified deposit
function extend(DepositId id, uint256 spendable_from_timestamp)
/// 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) {}
/// Lower deposit amount of funds from the specified deposit
function burn(DepositId id, uint256 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) {}
/// 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)
/// 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
@ -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