go-perun uses two contracts: the Adjudicator and the Assetholder. They are written in the contract language of Ethereum; solidity. Each contract must be deployed before go-perun can be used. Normally you would assume that they are already deployed and the addresses are known in advance. But since this is a complete example for a local chain, we must deploy them.


The Adjudicator contract ensures that a user can always enforce the rules of his channel. Since the main part of the communication is off-chain, he would only contact the Adjudicator if he feels betrayed by one of the participants.


The AssetHolder holds the on-chain balances for all ledger channels. It is always associated with a specific Adjudicator instance.

All participants deposit their funds into the AssetHolder before a channel can be opened. When the channel is closed all participants can withdraw their funds via the AssetHolder. In the case of a dispute, the AssetHolder respects the decision of its Adjudicator on how to proceed.

go-perun uses one contract per asset on top of the Ethereum blockchain. In this example we only use the ETHAssetHolder which is used for ether, the native currency in Ethereum. ERC20 Tokens are supported via the ERC20AssetHolder.


Deploying a contract means installing it on the blockchain. A deployed contract has a fixed public address. We will deploy both contracts as demonstration. In a running go-perun ecosystem the contracts’ addresses would be known in advance and you would just verify them.

First we have to deploy the Adjudicator and then use the Adjudicator’s address to deploy the AssetHolder:

func deployContracts(cb ethchannel.ContractBackend, acc accounts.Account) (adj, ah common.Address, err error) {
	// The context timeout must be atleast twice the blocktime.
	ctx, cancel := context.WithTimeout(context.Background(), 31*time.Second)
	defer cancel()

	adj, err = ethchannel.DeployAdjudicator(ctx, cb, acc)
	if err != nil {

	ah, err = ethchannel.DeployETHAssetholder(ctx, cb, adj, acc)

The context is needed to specify how long go-perun will wait for both deployments to succeed. If you have set a higher block time in ganache, you need to increase the timeout here too.


ganache-cli shows deployments as a Contract created transaction.


go-perun can verify the addresses of deployed contracts. There are verification methods for the Adjudicator and AssetHolderETH. In our example, it is enough to use ValidateAssetHolderETH since the AssetHolder validation function also implicitly validates the linked Adjudicator.


You should always verify a contract before using it to ensure that you don’t lose funds.

We wrap it in a function that accepts a ContractBackend and the addresses of the Adjudicator and Assetholder.

func validateContracts(cb ethchannel.ContractBackend, adj, ah common.Address) error {
	ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
	defer cancel()
	// Assetholder validation includes Adjudicator validation.
	return ethchannel.ValidateAssetHolderETH(ctx, cb, ah, adj)

Putting it together

Since we have two roles, we need to combine the two functions and switch on the role that is running:

func setupContracts(role Role, contractBackend ethchannel.ContractBackend, account accounts.Account) (adjudicator *ethchannel.Adjudicator, assetholder common.Address, err error) {
	var adjudicatorAddr common.Address
	// Alice will deploy the contracts and Bob validate them.
	if role == RoleAlice {
		adjudicatorAddr, assetholder, err = deployContracts(contractBackend, account)
		fmt.Println("Deployed contracts")
	} else {
		// Assume default addresses for Adjudicator and Assetholder.
		adjudicatorAddr = common.HexToAddress("0x079557d7549d7D44F4b00b51d2C532674129ed51")
		assetholder = common.HexToAddress("0x923439be515b6A928cB9650d70000a9044e49E85")
		err = validateContracts(contractBackend, adjudicatorAddr, assetholder)
		fmt.Println("Validated contracts")
	fmt.Printf(" Adjudicator at %v\n AssetHolder at %v\n", adjudicatorAddr, assetholder)
	adjudicator = ethchannel.NewAdjudicator(contractBackend, adjudicatorAddr, account.Address, account)

The addresses for Adjudicator and AssetHolder can be hard-coded again since we know what they will be when we deploy the contracts for the first time. This implies that we always restart the ganache-cli before running it.