Deployments

The Tinlake contracts are intended to be deployed once for every asset class. There is no shared code between any deployments to allow for a maximum of flexibility. We intend for users to customize and modify the codebase to better suit their needs. The codebase is clearly split into components that could and should be adjusted and core contracts. The core contracts enforce fairly basic rules (such as making sure a borrower can only unlock their NFT if they repaid their entire debt) while allowing other behaviors to be configured (such as how much interest the borrwer needs to pay).

The architecture for the contracts is built to make sure that the contracts intended to be customized have extremely simple interfaces and are purely limited to that functionality. By making these contracts small and simple, the amount of code that needs to be modified is minimized and thus the risk of bugs being introduced through that code is too.

The downside of deploying an entire set of new contracts every time is that the deployment itself needs to be secured and verified for each deployment. The deployment contracts are written to make this easy to do:

  • The deployment is completely deterministic and allows no runtime parameters that can be influenced in different transaction payloads.
  • The deployment can be verified for integrity by reading out the deployer contract state and verifying the contract bytecode. There is no need to scan transactions for any interference.
  • There are no special permissions in the deployment contract and the account used to deploy them does not keep any special properties. This means the deployment can be done from an "untrusted" account/computer.

Deployment Structure

There are three components used in the deploy process.

Fabs

To deploy each contract, a contract factory, called fab, is created first. The fabs are created because uploading the complete byte code for all contracts in one transaction exceeds the gas limit. Fabs can be deployed in separate transactions to allow for smaller transactions.

A fab typically adheres to the following pattern:

1contract CeilingFactory {
2 function newCeiling (address pile) public returns (address) {
3 Ceiling ceiling = new Ceiling(pile);
4 ceiling.rely(msg.sender);
5 ceiling.deny(address(this));
6 return address(ceiling);
7 }
8}

A fab always adds the sender as a ward to the contract and immediately removes itself. It returns an address of the created contract and can require arguments that are passed to the contract constructor.

Deployers

Deployments for the borrower contracts and the lender contracts are in two separate contracts. The deployer pattern is explained with a fictional example below.

The deployer contract has methods to call the fabs and create new contracts and wire them up correctly. The deployer contract requires all fab addresses to be passed into the deployer in the constructor. This means that the deployComponent methods don't need any authentication or can be executed in an incorrect way.

1contract Deployer {
2 address public root;
3 bytes32 public fooParam;
4 bytes32 pulbic barParam;
5 FooFab public fooFab;
6 BarFab public barFab;
7
8 address foo;
9 address bar;
10 bool activated;
11
12 constructor (address root_, bytes32 fooParam_, bytes32 barParam_, FooFab fooFab_, BarFab barFab_) public {
13 root = root_;
14 fooParam = fooParam_;
15 barParam = barParam_;
16 fooFab = fooFab_;
17 barFab = barFab;
18 }
19
20 function deployFoo() public {
21 // only allow deployFoo to be called once.
22 require(foo == address(0);
23 foo = fooFab.newFoo(fooParam);
24 foo.rely(root);
25 }
26
27 function deployBar() public {
28 // bar can only be deployed after foo is deployed.
29 require(bar == address(0) && foo != address(0));
30 bar.rely(root);
31 }
32
33 function deploy() public {
34 // ensure deploy() can only be called last and only once.
35 require(bar != address(0) && activated == false);
36 activated = true;
37 bar.activate();
38 }
39}

TinlakeRoot Contract

The TinlakeRoot takes both the lender and borrower deployer as arguments. It does the last step of connecting lender and borrower contracts and authorizing certain calls.

1contract TinlakeRoot is Auth {
2 BorrowerDeployer public borrowerDeployer;
3 LenderDeployer public lenderDeployer;
4
5 bool public deployed;
6 address public deployUsr;
7
8 constructor (address deployUsr_) public {
9 deployUsr = deployUsr_;
10 }
11
12 // --- Prepare ---
13 // Sets the two deployer dependencies. This needs to be called by the deployUsr;
14 function prepare(address lender_, address borrower_, address ward_) public {
15 require(deployUsr == msg.sender);
16 borrowerDeployer = BorrowerDeployer(borrower_);
17 lenderDeployer = LenderDeployer(lender_);
18 wards[ward_] = 1;
19 deployUsr = address(0); // disallow the deploy user to call this more than once.
20 }
21
22
23 // --- Deploy ---
24 function deploy() public {
25 require(address(borrowerDeployer) != address(0) && address(lenderDeployer) != address(0) && deployed == false);
26 deployed = true;
27
28 address distributor_ = lenderDeployer.distributor();
29 address poolValue = borrowerDeployer.pricePool();
30 DependLike(lenderDeployer.distributor()).depend("shelf", shelf_);
31 // [...]
32
33 }
34
35}

Governance Functions using the ward pattern

The TinlakeRoot also provides basic functions to change the behavior of the Tinlake contracts itself. It can designate additional wards on any contracts it is a ward on by exposing a method relyContract(address target, address usr) to call the target contract's rely function. Contract access can be revoked by calling denyContract(address target, address usr).

The root will allow any ward on the root to call this function. Once an address is a ward on another contract in the deployment it's file() and depend() can be called by it. This will allow modifying the system arbitrarily. Therefore wards on the TinlakeRoot have practically unlimited power in the system and should only be given to contracts that limit this by different ways (e.g. a DAO, a time lock or a multisig).