Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ServiceRegistry (without parameter reconfiguration) #1116

Merged
merged 40 commits into from
Jul 16, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
9764a2a
Compute the price movements
pirapira Jul 5, 2019
f3b0744
Maybe a single formula works better
pirapira Jul 5, 2019
8567da5
Formatted the exponentiation
pirapira Jul 5, 2019
0ef446f
Fix compilation
pirapira Jul 8, 2019
6dba747
Fixed tests
pirapira Jul 8, 2019
29f6e9b
Fixed tests
pirapira Jul 8, 2019
0903166
Use a constant instead of literals
pirapira Jul 8, 2019
1ba3e90
Rename owner to withdrawer
pirapira Jul 8, 2019
9b112ef
Use a more consistent min_price
pirapira Jul 8, 2019
5b1b6da
deploy_service_contracts() fail for older versions
pirapira Jul 8, 2019
ae72e12
Fix test
pirapira Jul 8, 2019
95b3406
Documented Deposit contract
pirapira Jul 9, 2019
4d99852
Rename a variable
pirapira Jul 9, 2019
6b70ba6
Documented ServiceRegistry and gave better names
pirapira Jul 9, 2019
6d96854
Add a test about withdrawing from Deposit
pirapira Jul 9, 2019
b27023f
Add tests for the new function
pirapira Jul 9, 2019
a715003
Increase test coverage
pirapira Jul 11, 2019
4b96f41
Remove a weird empty line
pirapira Jul 11, 2019
a430667
Check length before accessing elements
pirapira Jul 11, 2019
fc3b464
Check the number of constructor arguments
pirapira Jul 12, 2019
d6036d5
Add gas measurements for
pirapira Jul 12, 2019
0adaa62
Update raiden_contracts/data/source/services/ServiceRegistry.sol
pirapira Jul 15, 2019
41b6832
Remove ServiceRegistry.owner
pirapira Jul 15, 2019
b0ab66a
Remove duplicate validity extension
pirapira Jul 15, 2019
514c6b1
Fix help text
pirapira Jul 15, 2019
95b333e
Removed a test about ServiceRegistry.owner
pirapira Jul 15, 2019
137aba2
Document the return value policy
pirapira Jul 16, 2019
dfbbc1c
The old overflow check was not effective
pirapira Jul 16, 2019
f5e16ca
Fix a typo
pirapira Jul 16, 2019
8e64cce
Add a test for a too late withdraw from Deposit
pirapira Jul 16, 2019
b16bfa5
Test extension of ServiceRegistry registration
pirapira Jul 16, 2019
f085044
Document the event RegisteredService
pirapira Jul 16, 2019
0143fdc
Add error messages to ServiceRegistry
pirapira Jul 16, 2019
fcb2d56
Add boolean success flags
pirapira Jul 16, 2019
5f59f7a
Change the name of option for setting the initial
pirapira Jul 16, 2019
d002a6b
Deposit contract can disappear after withdrawal
pirapira Jul 16, 2019
8da6fba
Add error messages to Deposit contract's
pirapira Jul 16, 2019
9ec2205
Compile contracts
pirapira Jul 16, 2019
fc68cc1
ServiceRegistry.withdraw() cannot return anything
pirapira Jul 16, 2019
a7051ee
Camel-case ServiceRegistry
pirapira Jul 16, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ When you write Solidity code, be aware of the distinction between ``assert(cond)

``assert(cond)`` and ``require(cond)`` both cause a failure in the EVM execution when ``cond`` evaluates to 0. They use different EVM opcodes that cause different gas consumptions. More importantly, a convention dictates when to use which. Use ``assert(cond)`` only when you are confident that ``cond`` is always true. When an ``assert`` fires, that's considered as a bug in the Solidity program (or the Solidity compiler). For detecting invalid user inputs or invalid return values from other contracts, use ``require()``.

#### Returning a Boolean Indicating Success

We currently check the return values from all external function calls. In the Solidity code, all external function calls should happen within `require(...)` unless the function returns nothing.

When we implement a function that has nothing to return, we make the function always return true. So we have a more consistent visual look without naked calls.

#### Signature Convention

A signature should be useful only in one context. For this purpose, we follow a convention dictating the format of signed messages. The first fields of a signed message must look like::
Expand Down
4 changes: 4 additions & 0 deletions raiden_contracts/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
CONTRACT_SERVICE_REGISTRY = "ServiceRegistry"
CONTRACT_USER_DEPOSIT = "UserDeposit"
CONTRACT_ONE_TO_N = "OneToN"
CONTRACT_DEPOSIT = "Deposit"

# Timeouts
TEST_SETTLE_TIMEOUT_MIN = 5
Expand Down Expand Up @@ -51,6 +52,9 @@
# EndpointRegistry
EVENT_ADDRESS_REGISTERED = "AddressRegistered"

# ServiceRegistry
EVENT_REGISTERED_SERVICE = "RegisteredService"


class ChannelEvent(str, Enum):
OPENED = "ChannelOpened"
Expand Down
227 changes: 184 additions & 43 deletions raiden_contracts/data/contracts.json

Large diffs are not rendered by default.

26 changes: 14 additions & 12 deletions raiden_contracts/data/gas.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,22 @@
"CustomToken.transfer": 36123,
"CustomToken.transferFrom": 29557,
"MonitoringService.claimReward": 41931,
"MonitoringService.monitor": 207422,
"OneToN.claim": 92705,
"MonitoringService.monitor": 207736,
"OneToN.claim": 92786,
"SecretRegistry.registerSecret": 46892,
"TokenNetwork DEPLOYMENT": 4277369,
"TokenNetwork.closeChannel": 111155,
"ServiceRegistry.deposit": 433192,
"ServiceRegistry.setURL": 44555,
"TokenNetwork DEPLOYMENT": 4283201,
"TokenNetwork.closeChannel": 111439,
"TokenNetwork.openChannel": 97745,
"TokenNetwork.setTotalDeposit": 44909,
"TokenNetwork.setTotalWithdraw": 103306,
"TokenNetwork.settleChannel": 108229,
"TokenNetwork.unlock 1 locks": 31549,
"TokenNetwork.unlock 6 locks": 58649,
"TokenNetwork.updateNonClosingBalanceProof": 93786,
"TokenNetworkRegistry DEPLOYMENT": 4947971,
"TokenNetworkRegistry createERC20TokenNetwork": 3300096,
"TokenNetwork.setTotalDeposit": 44845,
"TokenNetwork.setTotalWithdraw": 103178,
"TokenNetwork.settleChannel": 108197,
"TokenNetwork.unlock 1 locks": 31485,
"TokenNetwork.unlock 6 locks": 58457,
"TokenNetwork.updateNonClosingBalanceProof": 93972,
"TokenNetworkRegistry DEPLOYMENT": 4953811,
"TokenNetworkRegistry createERC20TokenNetwork": 3304504,
"UserDeposit.deposit": 92392,
"UserDeposit.deposit (increase balance)": 32392,
"UserDeposit.planWithdraw": 64021,
Expand Down
166 changes: 145 additions & 21 deletions raiden_contracts/data/source/services/ServiceRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,49 +3,173 @@ pragma solidity 0.5.4;
import "raiden/Token.sol";
import "raiden/Utils.sol";

contract Deposit {
pirapira marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this it's own contract? Couldn't you also model this as a struct inside the ServiceRegistry contract?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, everything could be in ServiceRegistry and that would be gas-efficient.

The current choice follows security-in-depth. Even if ServiceRegistry is compromized, the deposits made earlier would not be affected.

Is this conceptual security worth more than the additional gas? I don't know for sure, but the deposits are rarer and bigger than usual Raiden transfers.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That argument makes sense to me, but I don't have much experience. The question was aimed at understanding the trade-off better!

// This contract holds ERC20 tokens as deposit until a predetemined point of time.

// The ERC20 token contract that the deposit is about.
Token public token;

// The address that can withdraw the deposit after the release time.
address public withdrawer;

// The timestamp after which the withdrawer can withdraw the deposit.
uint256 public release_at;

/// @param _token The address of the ERC20 token contract where the deposit is accounted.
/// @param _release_at The timestap after which the withdrawer can withdraw the deposit.
/// @param _withdrawer The address that can withdraw the deposit after the release time.
constructor(address _token, uint256 _release_at, address _withdrawer) public {
token = Token(_token);
// Don't care even if it's in the past.
release_at = _release_at;
withdrawer = _withdrawer;
}

// In order to make a deposit, transfer the ERC20 token into this contract.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to forbid this behaviour and only allow deposits with a deposit function, where one could check the token?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, contracts cannot refuse incoming ERC20 tokens.

In the beginning, I had a deposit() function, but it did nothing useful. So I removed it.

// If you transfer a wrong kind of ERC20 token or ETH into this contract,
// these tokens will be lost forever.

/// @notice Withdraws the tokens that have been deposited.
/// Only `withdrawer` can call this.
/// @param _to The address where the withdrawn tokens should go.
function withdraw(address payable _to) external {
uint256 balance = token.balanceOf(address(this));
require(msg.sender == withdrawer, "the caller is not the withdrawer");
require(now >= release_at, "deposit not released yet");
require(balance > 0, "nothing to withdraw");
require(token.transfer(_to, balance), "token didn't transfer");
selfdestruct(_to); // The contract can disappear.
}
}


contract ServiceRegistry is Utils {
Token public token;
address public owner;

mapping(address => uint256) public deposits; // token amount staked by the service provider
// After a price is set to set_price at timestamp set_price_at,
// the price decays according to decayed_price().
uint256 public set_price;
uint256 public set_price_at;

// Once the price is at min_price, it can't decay further.
uint256 constant min_price = 1000;

mapping(address => uint256) public service_valid_till;
mapping(address => string) public urls; // URLs of services for HTTP access
address[] public service_addresses; // list of available services (ethereum addresses)

// @param service The address of the registered service provider
// @param valid_till The timestamp of the moment when the registration expires
// @param deposit_amount The amount of deposit transferred
// @param deposit The address of Deposit instance where the deposit is stored.
event RegisteredService(address indexed service, uint256 valid_till, uint256 deposit_amount, Deposit deposit_contract);
pirapira marked this conversation as resolved.
Show resolved Hide resolved
pirapira marked this conversation as resolved.
Show resolved Hide resolved

// @param _token_for_registration The address of the ERC20 token contract that services use for registration fees
constructor(address _token_for_registration) public {
constructor(address _token_for_registration, uint256 _initial_price) public {
pirapira marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should min_price be settable as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This follows in the next PR. min_price, 6/5 bumping ratio and 200 days will be configurable.

require(_token_for_registration != address(0x0), "token at address zero");
require(contractExists(_token_for_registration), "token has no code");
require(_initial_price >= min_price, "initial price too low");

token = Token(_token_for_registration);
// Check if the contract is indeed a token contract
require(token.totalSupply() > 0, "total supply zero");
owner = msg.sender;

// Set up the price and the set price timestamp
set_price = _initial_price;
set_price_at = now;
}

function deposit(uint amount) public {
require(amount > 0);
// @notice Locks tokens and registers a service or extends the registration.
// @param _limit_amount The biggest amount of tokens that the caller is willing to deposit.
// The call fails if the current price is higher (this is always possible
// when other parties have just called `deposit()`).
function deposit(uint _limit_amount) public returns (bool _success) {
uint256 amount = current_price();
require(_limit_amount >= amount, "not enough limit");

// Extend the service position.
uint256 valid_till = service_valid_till[msg.sender];
if (valid_till < now) { // a first time joiner or an expired service.
valid_till = now;
}
// Check against overflow.
require(valid_till < valid_till + 180 days, "overflow during extending the registration");
valid_till = valid_till + 180 days;
pirapira marked this conversation as resolved.
Show resolved Hide resolved
assert(valid_till > service_valid_till[msg.sender]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about low-level differences, but why do you use assert here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because I know the assert is always true. So I use assert and then the SMT solver in solc tries to find a counterexample.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To say it in a different way: assert should only fail for bugs in the contract, require is for all other cases.

service_valid_till[msg.sender] = valid_till;

// Record the price
set_price = amount * 6 / 5;
set_price_at = now;

// Move the deposit in a new Deposit contract.
Deposit depo = new Deposit(address(token), valid_till, msg.sender);
require(token.transferFrom(msg.sender, address(depo), amount), "Token transfer for deposit failed");

// This also allows for MSs to deposit and use other MSs
deposits[msg.sender] += amount;
// Fire event
emit RegisteredService(msg.sender, valid_till, amount, depo);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Above you write that all functions should return true, so this one as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe so. I'll add return true.


// Transfer the deposit to the smart contract
require(token.transferFrom(msg.sender, address(this), amount), "tokens did not transfer");
return true;
}

/// Set the URL used to access a service via HTTP.
/// When this is called for the first time, the service's ethereum address
/// is also added to `service_addresses`.
function setURL(string memory new_url) public {
/// @notice Sets the URL used to access a service via HTTP.
/// Only a currently registered service can call this successfully.
/// @param new_url The new URL string to be stored.
function setURL(string memory new_url) public returns (bool _success) {
require(now < service_valid_till[msg.sender], "registration expired");
require(bytes(new_url).length != 0, "new url is empty string");
if (bytes(urls[msg.sender]).length == 0) {
service_addresses.push(msg.sender);
}
urls[msg.sender] = new_url;
return true;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, should this return true?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's also true.


/// The amount of time till the price decreases to roughly 1/e.
uint constant decay_constant = 200 days; // Maybe make this configurable?

/// @notice Calculates the decreased price after a number of seconds.
/// @param _set_price The initial price.
/// @param _seconds_passed The number of seconds passed since the initial
/// price was set.
function decayed_price(uint256 _set_price, uint256 _seconds_passed) public
view returns (uint256) {
// We are here trying to approximate some exponential decay.
// exp(- X / A) where
// X is the number of seconds since the last price change
// A is the decay constant (A = 200 days corresponds to 0.5% decrease per day)

// exp(- X / A) ~~ P / Q where
// P = 24 A^4
// Q = 24 A^4 + 24 A^3X + 12 A^2X^2 + 4 AX^3 + X^4
// Note: swap P and Q, and then think about the Taylor expansion.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice approach, works great!


uint256 X = _seconds_passed;

if (X >= 2 ** 60) { // The computation below overflows.
return min_price;
pirapira marked this conversation as resolved.
Show resolved Hide resolved
}

uint256 A = decay_constant;

uint256 P = 24 * (A ** 4);
uint256 Q = P + 24*(A**3)*X + 12*(A**2)*(X**2) + 4*A*(X**3) + X**4;

uint256 price = _set_price * P / Q;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once you make A configurable, you might have to check for an overflow here as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the next PR will contain lots of those checks. I hope nobody needs the price decaying over billions of years.


// Not allowing a price smaller than 1000.
// Once it's too low it's too low forever.
if (price < min_price) { // Maybe make this configurable too?
price = min_price;
}
return price;
}

/// Returns number of registered services. Useful for accessing service_addresses.
function serviceCount() public view returns(uint) {
return service_addresses.length;
/// @notice The amount of deposits for registration or extension.
/// Note: the price moves quickly depending on what other addresses do.
/// The current price might change after you send a `deposit()` transaction
/// before the transaction is executed.
function current_price() public view returns (uint256) {
require(now >= set_price_at, "An underflow in price computation");
uint256 seconds_passed = now - set_price_at;

return decayed_price(set_price, seconds_passed);
}
}

Expand Down
11 changes: 10 additions & 1 deletion raiden_contracts/deploy/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,12 @@ def raiden(
type=int,
help="Maximum amount of tokens deposited in UserDeposit",
)
@click.option(
"--initial-service-registration-price",
required=True,
type=int,
help="Initial amount of deposit for a registration in ServiceRegistry",
)
@click.option("--save-info/--no-save-info", default=True, help="Save deployment info to a file.")
@click.pass_context
def services(
Expand All @@ -221,12 +227,15 @@ def services(
save_info: bool,
contracts_version: Optional[str],
user_deposit_whole_limit: int,
initial_service_registration_price: int,
) -> None:
setup_ctx(ctx, private_key, rpc_provider, wait, gas_price, gas_limit, contracts_version)
deployer: ContractDeployer = ctx.obj["deployer"]

deployed_contracts_info = deployer.deploy_service_contracts(
token_address=token_address, user_deposit_whole_balance_limit=user_deposit_whole_limit
token_address=token_address,
user_deposit_whole_balance_limit=user_deposit_whole_limit,
initial_service_registration_price=initial_service_registration_price,
)
deployed_contracts = {
contract_name: info["address"]
Expand Down
21 changes: 18 additions & 3 deletions raiden_contracts/deploy/contract_deployer.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@
from raiden_contracts.utils.file_ops import load_json_from_path
from raiden_contracts.utils.signature import private_key_to_address
from raiden_contracts.utils.transaction import check_successful_tx
from raiden_contracts.utils.versions import contracts_version_expects_deposit_limits
from raiden_contracts.utils.versions import (
contracts_version_expects_deposit_limits,
contracts_version_has_initial_service_deposit,
)

LOG = getLogger(__name__)

Expand Down Expand Up @@ -303,17 +306,29 @@ def _register_token_network_with_limits(
return token_network_address

def deploy_service_contracts(
self, token_address: HexAddress, user_deposit_whole_balance_limit: int
self,
token_address: HexAddress,
user_deposit_whole_balance_limit: int,
initial_service_registration_price: int,
) -> DeployedContracts:
"""Deploy 3rd party service contracts"""
if not contracts_version_has_initial_service_deposit(
self.contract_manager.contracts_version
):
raise RuntimeError("Deployment of older service contracts is not suppported.")

chain_id = int(self.web3.version.network)
deployed_contracts: DeployedContracts = {
"contracts_version": self.contract_manager.contracts_version,
"chain_id": chain_id,
"contracts": {},
}

self._deploy_and_remember(CONTRACT_SERVICE_REGISTRY, [token_address], deployed_contracts)
self._deploy_and_remember(
CONTRACT_SERVICE_REGISTRY,
[token_address, initial_service_registration_price],
deployed_contracts,
)
user_deposit = self._deploy_and_remember(
contract_name=CONTRACT_USER_DEPOSIT,
arguments=[token_address, user_deposit_whole_balance_limit],
Expand Down
16 changes: 10 additions & 6 deletions raiden_contracts/deploy/contract_verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,13 @@ def verify_deployment_data(self, deployment_data: DeployedContracts) -> bool:
!= secret_registry.address
):
raise RuntimeError("secret_registry_address onchain has an unexpected value.")
if len(constructor_arguments) != 5:
raise RuntimeError(
"TokenNetworkRegistry received a wrong number of constructor arguments."
)
if secret_registry.address != constructor_arguments[0]:
raise RuntimeError(
"TokenNetwork's constructor received a different SecretRegistry address."
"TokenNetworkRegistry's constructor received a different SecretRegistry address."
)
if token_network_registry.functions.chain_id().call() != constructor_arguments[1]:
raise RuntimeError("TokenNetwork remembers a wrong chain_id.")
Expand Down Expand Up @@ -348,14 +352,14 @@ def _verify_service_registry_deployment(
) -> None:
""" Check an onchain deployment of ServiceRegistry and constructor arguments """
if to_checksum_address(service_registry.functions.token().call()) != token_address:
raise RuntimeError("service_registry has a wrong token address")
raise RuntimeError("ServiceRegistry has a wrong token address")
if len(constructor_arguments) != 2:
raise RuntimeError(
"ServiceRegistry was deployed with a wrong number of constructor arguments"
)
if token_address != constructor_arguments[0]:
raise RuntimeError(
f"expected token addredd {token_address} "
f"but the constructor argument for {CONTRACT_SERVICE_REGISTRY} is "
f"{constructor_arguments[0]}"
)
if len(constructor_arguments) != 1:
raise RuntimeError(
"service_registry was deployed with a wrong number of constructor arguments"
)
1 change: 1 addition & 0 deletions raiden_contracts/tests/fixtures/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from .base import *
from .channel import *
from .contracts import *
from .deposit_fixtures import *
from .monitoring_service import *
from .one_to_n import *
from .secret_registry import *
Expand Down
16 changes: 16 additions & 0 deletions raiden_contracts/tests/fixtures/deposit_fixtures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from typing import Callable, List

import pytest
from web3.contract import Contract

from raiden_contracts.constants import CONTRACT_DEPOSIT


@pytest.fixture
def get_deposit_contract(deploy_tester_contract: Callable) -> Callable:
"""Deploy a Deposit contract with the given arguments"""

def get(arguments: List) -> Contract:
return deploy_tester_contract(CONTRACT_DEPOSIT, arguments)

return get
Loading