Skip to content

Ethereum Contracts: Architecture

Sid Sethi edited this page Oct 29, 2020 · 1 revision

Table of Contents

Audius Token

The Audius token used for staking is deployed on Ethereum mainnet with the ERC20 token standard, built using OpenZeppelin's implementation and the following extensions:

  • ERC20Detailed - gives the token additional properties name, symbol, and decimals
  • ERC20Mintable - adds a set of accounts that have permission to mint new tokens
  • ERC20Pausable - enables pausing/unpausing of transfers and allowances

The token is deployed with the following properties:

  • Name = "TestAudius"
  • Symbol = "AUDS"
  • Decimals = 18 (standard - imitates relationship between Ether and Wei)
  • Initial Supply - 1,000,000,000 AUDS (1 billion AUDS = 10^9 AUDS * 10^18 decimal places = 10^27)

The token contract code can be viewed on GitHub.

Note: during private beta these tokens are deployed as 'TestAudius' with the ticker TAUDS but will be referred to within this document as AUDS (the intended final ticker).

Important Addresses & Their Functions

Deployer

Address from which contracts are initially deployed. This is the address from which the proxy contract can be upgraded and manipulated. Reference tests in the eth-contracts repository for examples.

Treasury

Address from which rewards are transferred for stakers. The current value of the rewards in the staking contract can be viewed as equivalent to the amount staked 'for' the treasury address. This is also the address used to set the 'staking owner' - restricting the function calls around staking to only be callable from the service provider factory in order to guarantee a 1:1 association of staking and service provider registration. The treasury also controls the min and max stake amount.

Versioner

Address from which expected service versions are set. It is important to note that this address is set during the deployment of the VersioningFactory and is not assumed to be the deployer of the contract

Versioning

Versioning is handled within the audius network by a distinct set smart contracts deployed to the ethereum main network - one storing an official record of service versions (VersioningFactory), one allowing service providers to register and stake (ServiceProviderFactory + Staking) to participate in the audius network.

Services within the audius network are typed as follows:

  • discovery-provider
  • creator-node
  • (...potentially more)

The above service types types are treated as keys and paired with a basic semantic versioning value, for example:

<"discovery-provider", "0.0.1">

VersioningFactory stores a record of service types and corresponding versions - this is represented on chain in the following mapping format:

<"discovery-provider", ["0.0.1", "0.0.2", ..., "currentVersion"]>

Each service that is running within the audius network is required to expose a '/version' endpoint that corresponds to a valid registered version of the service (example). If no corresponding version is found, this deployed service instance is considered invalid and will not be selected by clients.

Example of /version endpoint response:

{
	"service": "discovery-provider",
	"version": "0.0.1",
	...
}

ServiceProviderFactory + Staking

Service providers can register on the audius network with the following:

  • Valid ethereum wallet address w/AUDS tokens
  • Active service endpoint
  • Known service type
  • Amount of AUDS tokens to stake

An important point is that both registration and staking are one operation - a service provider cannot stake without registering a service endpoint and vice versa. To that end, the staking contract only accepts stake / unstake operations from the ServiceProviderFactory.

Once a service provider endpoint has been confirmed as valid (exposing a /version endpoint per above) the information regarding the service provider (wallet address, service type, endpoint) are stored in on-chain storage and the AUDS tokens are staked on behalf of the provided wallet address in the staking contract.

Once a provider has been registered, the owner has the option to increase / decrease the amount they have staked. When a provider 'deregisters' their content, the amount staked is returned to the calling wallet and their endpoint is removed from ServiceProviderFactory storage.

Note that there are protocol-level bounds such as min/max stake which are set by the Staking contract. There is also a 1:1 restriction on

Staking Implementation

Our staking implementation adheres to the EIP900 spec and is modeled as an 'Ownedupgradeable Proxy' contract - allowing us to upgrade the staking contract logic independent of the stored contents.

Why not adopt a similar registry pattern for this contract similar to the rest of our code? The reason we decided to adopt this pattern was to enable in place logic updates without transferring the staked tokens between contracts - with the decoupled storage pattern, an entirely new contract is deployed, and while storage can be persisted between registry contract deployment, the complexity tradeoff led us to implement this as an unstructured storage contract. However, this contract is still registered - in this way the ServiceProviderFactory is made aware of the Staking contract's location, adhering to our existing pattern.

The actual staking logic contract is a significantly modified version of AragonOS' staking implementation.

References

Deployment Flow

Deployment of the staking contract and proxy has a few steps involved - these are performed in the file 4_staking.js and are outlined here

  1. Deploy the staking contract
  • At this time, the staking contract is 'uninitialized' and cannot be interacted with
  1. Deploy the proxy contract and add it to the registry

  2. Encode the function call necessary to initialize the proxy contract - this is done using abi.RawEncode and abi.methodID

  • In this case, the function call is 'initialize' and expects the token address as well as the treasury address
  1. Invoke the encoded function on the proxy contract with the 'upgradeToAndCall' function
  • This function sets the implementation address for the proxy contract - this is how the staking contract functions can be invoked with the fallback delegatecall pattern (reference Proxy.sol fallback external payable function)
  • After upgrading to the new implementation, the encoded function 'initialize' is called on the new implementation address
    • This function sets the treasury address, relevant token address, and initializes the claim parameters + min/max bounds
    • A contract cannot be reinitialized after this point
  • At this point, the proxy contract can be addressed with the 'Staking.sol' ABI and storage changes will persist after a logic contract upgrade

Notes on upgrade:

  • Changes can be made to the staking logic as described above modifying the existing staking contract WITH ALL STORAGE VARIABLES RETAINING ORDER and invoking the 'upgradeTo' function with the new implementation address

Claims

Currently, claiming and network rewards are handled through the treasury address - a claim round is initialized by the function 'fundNewClaim' - this function call transfers the requested amount from the msg.sender address to the treasuryAddress' staked balance. The two fields of 'currentClaimBlock' and 'currentClaimableAmount' are updated to non-zero values, enabling service providers to claim rewards.

Each service's claim amount is calculated by their proportional holdings at the time when the claim was funded (detailed here) - any service registering after said funding will have to wait until the next round to claim rewards. A single claim per funding window is the maximum per service provider owner.

While funding is done manually for now, we will eventually move to a minting + automatic periodic claim fund model.

How does this flow look for a client using the audius-lib?

  • Query the expected version for each service we are connecting to from the VersioningFactory contract and cache this information locally
    • For example, "discovery-provider" might return a value of "0.0.2"
  • Query known endpoints for a given service type from the chain
    • Exposed function from ServiceProviderFactory
  • Given a list of service provider endpoints, query each service's deployed version from the /version endpoint
  • If one or more deployed services correspond to the latest version for the same service type, select from this list of up to date service endpoints
    • Current selection criteria is random, but will be modified to filter on regions, stake amount, etc.
  • If no services correspond to the latest version, query the prior valid version and cross-reference with the list of service provider endpoints
    • Keep going backwards until a service with a valid prior version is found
    • For example, discovery-provider /version endpoints returning "0.0.1" will be selected if no "0.0.2" is found
  • If no valid services are found for a given service type (i.e. "discovery-provider"), an ERROR state has been reached and no endpoint will be selected
    • This prevents services with an unknown version from being selected by clients and exhibiting unknown behavior
    • Note that Audius, the company, will be running an up to date version of each service provider which should prevent this error state from being reached