Skip to content

Commit

Permalink
Static link token (#15546)
Browse files Browse the repository at this point in the history
* Static link token

* More testing, support in CCIP

* Couple smoke tests

* More defense

* Comments
  • Loading branch information
connorwstein authored Dec 6, 2024
1 parent fe8639a commit 4a0e8f0
Show file tree
Hide file tree
Showing 17 changed files with 360 additions and 32 deletions.
23 changes: 23 additions & 0 deletions deployment/address_book.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,3 +271,26 @@ func AddressBookContains(ab AddressBook, chain uint64, addrToFind string) (bool,

return false, nil
}

// AddressesContainBundle checks if the addresses
// contains a single instance of all the addresses in the bundle.
// It returns an error if there are more than one instance of a contract.
func AddressesContainBundle(addrs map[string]TypeAndVersion, wantTypes map[TypeAndVersion]struct{}) (bool, error) {
counts := make(map[TypeAndVersion]int)
for wantType := range wantTypes {
for _, haveType := range addrs {
if wantType == haveType {
counts[wantType]++
if counts[wantType] > 1 {
return false, fmt.Errorf("found more than one instance of contract %s", wantType)
}
}
}
}
// Either 0 or 1, so we can just check the sum.
sum := 0
for _, count := range counts {
sum += count
}
return sum == len(wantTypes), nil
}
33 changes: 33 additions & 0 deletions deployment/address_book_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,3 +243,36 @@ func TestAddressBook_ConcurrencyAndDeadlock(t *testing.T) {

wg.Wait()
}

func TestAddressesContainsBundle(t *testing.T) {
onRamp100 := NewTypeAndVersion("OnRamp", Version1_0_0)
onRamp110 := NewTypeAndVersion("OnRamp", Version1_1_0)
onRamp120 := NewTypeAndVersion("OnRamp", Version1_2_0)
addr1 := common.HexToAddress("0x1").String()
addr2 := common.HexToAddress("0x2").String()
addr3 := common.HexToAddress("0x3").String()

// More than one instance should error
_, err := AddressesContainBundle(map[string]TypeAndVersion{
addr1: onRamp100,
addr2: onRamp100,
}, map[TypeAndVersion]struct{}{onRamp100: {}})
require.Error(t, err)

// No such instances should be false
exists, err := AddressesContainBundle(map[string]TypeAndVersion{
addr2: onRamp110,
addr1: onRamp110,
}, map[TypeAndVersion]struct{}{onRamp100: {}})
require.NoError(t, err)
assert.Equal(t, exists, false)

// 2 elements
exists, err = AddressesContainBundle(map[string]TypeAndVersion{
addr1: onRamp100,
addr2: onRamp110,
addr3: onRamp120,
}, map[TypeAndVersion]struct{}{onRamp100: {}, onRamp110: {}})
require.NoError(t, err)
assert.Equal(t, exists, true)
}
25 changes: 20 additions & 5 deletions deployment/ccip/changeset/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ var (
type CCIPChainState struct {
commoncs.MCMSWithTimelockState
commoncs.LinkTokenState
commoncs.StaticLinkTokenState
OnRamp *onramp.OnRamp
OffRamp *offramp.OffRamp
FeeQuoter *fee_quoter.FeeQuoter
Expand Down Expand Up @@ -219,16 +220,23 @@ func (c CCIPChainState) GenerateView() (view.ChainView, error) {
chainView.MCMSWithTimelock = mcmsView
}
if c.LinkToken != nil {
linkTokenView, err := common_v1_0.GenerateLinkTokenView(c.LinkToken)
linkTokenView, err := c.GenerateLinkView()
if err != nil {
return chainView, errors.Wrapf(err, "failed to generate link token view for link token %s", c.LinkToken.Address().String())
}
chainView.LinkToken = linkTokenView
}
if c.StaticLinkToken != nil {
staticLinkTokenView, err := c.GenerateStaticLinkView()
if err != nil {
return chainView, err
}
chainView.StaticLinkToken = staticLinkTokenView
}
return chainView, nil
}

// Onchain state always derivable from an address book.
// CCIPOnChainState state always derivable from an address book.
// Offchain state always derivable from a list of nodeIds.
// Note can translate this into Go struct needed for MCMS/Docs/UI.
type CCIPOnChainState struct {
Expand Down Expand Up @@ -284,24 +292,31 @@ func LoadOnchainState(e deployment.Environment) (CCIPOnChainState, error) {
// LoadChainState Loads all state for a chain into state
func LoadChainState(chain deployment.Chain, addresses map[string]deployment.TypeAndVersion) (CCIPChainState, error) {
var state CCIPChainState
mcmsWithTimelock, err := commoncs.LoadMCMSWithTimelockState(chain, addresses)
mcmsWithTimelock, err := commoncs.MaybeLoadMCMSWithTimelockState(chain, addresses)
if err != nil {
return state, err
}
state.MCMSWithTimelockState = *mcmsWithTimelock

linkState, err := commoncs.LoadLinkTokenState(chain, addresses)
linkState, err := commoncs.MaybeLoadLinkTokenState(chain, addresses)
if err != nil {
return state, err
}
state.LinkTokenState = *linkState
staticLinkState, err := commoncs.MaybeLoadStaticLinkTokenState(chain, addresses)
if err != nil {
return state, err
}
state.StaticLinkTokenState = *staticLinkState
for address, tvStr := range addresses {
switch tvStr.String() {
case deployment.NewTypeAndVersion(commontypes.RBACTimelock, deployment.Version1_0_0).String(),
deployment.NewTypeAndVersion(commontypes.ProposerManyChainMultisig, deployment.Version1_0_0).String(),
deployment.NewTypeAndVersion(commontypes.CancellerManyChainMultisig, deployment.Version1_0_0).String(),
deployment.NewTypeAndVersion(commontypes.BypasserManyChainMultisig, deployment.Version1_0_0).String(),
deployment.NewTypeAndVersion(commontypes.LinkToken, deployment.Version1_0_0).String():
deployment.NewTypeAndVersion(commontypes.LinkToken, deployment.Version1_0_0).String(),
deployment.NewTypeAndVersion(commontypes.StaticLinkToken, deployment.Version1_0_0).String():
// Skip common contracts, they are already loaded.
continue
case deployment.NewTypeAndVersion(CapabilitiesRegistry, deployment.Version1_0_0).String():
cr, err := capabilities_registry.NewCapabilitiesRegistry(common.HexToAddress(address), chain.Client)
Expand Down
24 changes: 24 additions & 0 deletions deployment/ccip/changeset/state_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package changeset

import (
"testing"

"github.com/stretchr/testify/require"

"github.com/smartcontractkit/chainlink/deployment/environment/memory"
"github.com/smartcontractkit/chainlink/v2/core/logger"
)

func TestSmokeState(t *testing.T) {
lggr := logger.TestLogger(t)
tenv := NewMemoryEnvironmentWithJobsAndContracts(t, lggr, memory.MemoryEnvironmentConfig{
Chains: 3,
Nodes: 4,
Bootstraps: 1,
NumOfUsersPerChain: 1,
}, nil)
state, err := LoadOnchainState(tenv.Env)
require.NoError(t, err)
_, err = state.View(tenv.Env.AllChainSelectors())
require.NoError(t, err)
}
22 changes: 22 additions & 0 deletions deployment/ccip/changeset/view_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package changeset

import (
"testing"

"github.com/stretchr/testify/require"

"github.com/smartcontractkit/chainlink/deployment/environment/memory"
"github.com/smartcontractkit/chainlink/v2/core/logger"
)

func TestSmokeView(t *testing.T) {
lggr := logger.TestLogger(t)
tenv := NewMemoryEnvironmentWithJobsAndContracts(t, lggr, memory.MemoryEnvironmentConfig{
Chains: 3,
Nodes: 4,
Bootstraps: 1,
NumOfUsersPerChain: 1,
}, nil)
_, err := ViewCCIP(tenv.Env)
require.NoError(t, err)
}
1 change: 1 addition & 0 deletions deployment/ccip/view/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type ChainView struct {
CapabilityRegistry map[string]common_v1_0.CapabilityRegistryView `json:"capabilityRegistry,omitempty"`
MCMSWithTimelock common_v1_0.MCMSWithTimelockView `json:"mcmsWithTimelock,omitempty"`
LinkToken common_v1_0.LinkTokenView `json:"linkToken,omitempty"`
StaticLinkToken common_v1_0.StaticLinkTokenView `json:"staticLinkToken,omitempty"`
}

func NewChain() ChainView {
Expand Down
10 changes: 3 additions & 7 deletions deployment/common/changeset/deploy_link_token_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package changeset_test
import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap/zapcore"

Expand All @@ -28,12 +27,9 @@ func TestDeployLinkToken(t *testing.T) {
require.NoError(t, err)
addrs, err := e.ExistingAddresses.AddressesForChain(chain1)
require.NoError(t, err)
state, err := changeset.LoadLinkTokenState(e.Chains[chain1], addrs)
state, err := changeset.MaybeLoadLinkTokenState(e.Chains[chain1], addrs)
require.NoError(t, err)
view, err := state.GenerateLinkView()
// View itself already unit tested
_, err = state.GenerateLinkView()
require.NoError(t, err)
assert.Equal(t, view.Owner, e.Chains[chain1].DeployerKey.From)
assert.Equal(t, view.TypeAndVersion, "LinkToken 1.0.0")
// Initially nothing minted.
assert.Equal(t, view.Supply.String(), "0")
}
2 changes: 1 addition & 1 deletion deployment/common/changeset/internal/mcms_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func TestDeployMCMSWithTimelockContracts(t *testing.T) {
addresses, err := ab.AddressesForChain(chainsel.TEST_90000001.Selector)
require.NoError(t, err)
require.Len(t, addresses, 4)
mcmsState, err := changeset.LoadMCMSWithTimelockState(chains[chainsel.TEST_90000001.Selector], addresses)
mcmsState, err := changeset.MaybeLoadMCMSWithTimelockState(chains[chainsel.TEST_90000001.Selector], addresses)
require.NoError(t, err)
v, err := mcmsState.GenerateMCMSWithTimelockView()
b, err := json.MarshalIndent(v, "", " ")
Expand Down
82 changes: 74 additions & 8 deletions deployment/common/changeset/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,31 @@ package changeset

import (
"errors"
"fmt"

"github.com/ethereum/go-ethereum/common"
owner_helpers "github.com/smartcontractkit/ccip-owner-contracts/pkg/gethwrappers"

"github.com/smartcontractkit/chainlink/deployment"
"github.com/smartcontractkit/chainlink/deployment/common/types"
"github.com/smartcontractkit/chainlink/deployment/common/view/v1_0"
"github.com/smartcontractkit/chainlink/v2/core/gethwrappers/generated/link_token_interface"
"github.com/smartcontractkit/chainlink/v2/core/gethwrappers/shared/generated/link_token"
)

// MCMSWithTimelockState holds the Go bindings
// for a MCMSWithTimelock contract deployment.
// It is public for use in product specific packages.
// Either all fields are nil or all fields are non-nil.
type MCMSWithTimelockState struct {
CancellerMcm *owner_helpers.ManyChainMultiSig
BypasserMcm *owner_helpers.ManyChainMultiSig
ProposerMcm *owner_helpers.ManyChainMultiSig
Timelock *owner_helpers.RBACTimelock
}

// Validate checks that all fields are non-nil, ensuring it's ready
// for use generating views or interactions.
func (state MCMSWithTimelockState) Validate() error {
if state.Timelock == nil {
return errors.New("timelock not found")
Expand Down Expand Up @@ -66,29 +71,51 @@ func (state MCMSWithTimelockState) GenerateMCMSWithTimelockView() (v1_0.MCMSWith
}, nil
}

func LoadMCMSWithTimelockState(chain deployment.Chain, addresses map[string]deployment.TypeAndVersion) (*MCMSWithTimelockState, error) {
// MaybeLoadMCMSWithTimelockState looks for the addresses corresponding to
// contracts deployed with DeployMCMSWithTimelock and loads them into a
// MCMSWithTimelockState struct. If none of the contracts are found, the state struct will be nil.
// An error indicates:
// - Found but was unable to load a contract
// - It only found part of the bundle of contracts
// - If found more than one instance of a contract (we expect one bundle in the given addresses)
func MaybeLoadMCMSWithTimelockState(chain deployment.Chain, addresses map[string]deployment.TypeAndVersion) (*MCMSWithTimelockState, error) {
state := MCMSWithTimelockState{}
// We expect one of each contract on the chain.
timelock := deployment.NewTypeAndVersion(types.RBACTimelock, deployment.Version1_0_0)
proposer := deployment.NewTypeAndVersion(types.ProposerManyChainMultisig, deployment.Version1_0_0)
canceller := deployment.NewTypeAndVersion(types.CancellerManyChainMultisig, deployment.Version1_0_0)
bypasser := deployment.NewTypeAndVersion(types.BypasserManyChainMultisig, deployment.Version1_0_0)

// Ensure we either have the bundle or not.
_, err := deployment.AddressesContainBundle(addresses,
map[deployment.TypeAndVersion]struct{}{
timelock: {}, proposer: {}, canceller: {}, bypasser: {},
})
if err != nil {
return nil, fmt.Errorf("unable to check MCMS contracts on chain %s error: %w", chain.Name(), err)
}

for address, tvStr := range addresses {
switch tvStr.String() {
case deployment.NewTypeAndVersion(types.RBACTimelock, deployment.Version1_0_0).String():
switch tvStr {
case timelock:
tl, err := owner_helpers.NewRBACTimelock(common.HexToAddress(address), chain.Client)
if err != nil {
return nil, err
}
state.Timelock = tl
case deployment.NewTypeAndVersion(types.ProposerManyChainMultisig, deployment.Version1_0_0).String():
case proposer:
mcms, err := owner_helpers.NewManyChainMultiSig(common.HexToAddress(address), chain.Client)
if err != nil {
return nil, err
}
state.ProposerMcm = mcms
case deployment.NewTypeAndVersion(types.BypasserManyChainMultisig, deployment.Version1_0_0).String():
case bypasser:
mcms, err := owner_helpers.NewManyChainMultiSig(common.HexToAddress(address), chain.Client)
if err != nil {
return nil, err
}
state.BypasserMcm = mcms
case deployment.NewTypeAndVersion(types.CancellerManyChainMultisig, deployment.Version1_0_0).String():
case canceller:
mcms, err := owner_helpers.NewManyChainMultiSig(common.HexToAddress(address), chain.Client)
if err != nil {
return nil, err
Expand All @@ -110,10 +137,17 @@ func (s LinkTokenState) GenerateLinkView() (v1_0.LinkTokenView, error) {
return v1_0.GenerateLinkTokenView(s.LinkToken)
}

func LoadLinkTokenState(chain deployment.Chain, addresses map[string]deployment.TypeAndVersion) (*LinkTokenState, error) {
func MaybeLoadLinkTokenState(chain deployment.Chain, addresses map[string]deployment.TypeAndVersion) (*LinkTokenState, error) {
state := LinkTokenState{}
linkToken := deployment.NewTypeAndVersion(types.LinkToken, deployment.Version1_0_0)
// Perhaps revisit if we have a use case for multiple.
_, err := deployment.AddressesContainBundle(addresses, map[deployment.TypeAndVersion]struct{}{linkToken: {}})
if err != nil {
return nil, fmt.Errorf("unable to check link token on chain %s error: %w", chain.Name(), err)
}
for address, tvStr := range addresses {
if tvStr.String() == deployment.NewTypeAndVersion(types.LinkToken, deployment.Version1_0_0).String() {
switch tvStr {
case linkToken:
lt, err := link_token.NewLinkToken(common.HexToAddress(address), chain.Client)
if err != nil {
return nil, err
Expand All @@ -123,3 +157,35 @@ func LoadLinkTokenState(chain deployment.Chain, addresses map[string]deployment.
}
return &state, nil
}

type StaticLinkTokenState struct {
StaticLinkToken *link_token_interface.LinkToken
}

func (s StaticLinkTokenState) GenerateStaticLinkView() (v1_0.StaticLinkTokenView, error) {
if s.StaticLinkToken == nil {
return v1_0.StaticLinkTokenView{}, errors.New("static link token not found")
}
return v1_0.GenerateStaticLinkTokenView(s.StaticLinkToken)
}

func MaybeLoadStaticLinkTokenState(chain deployment.Chain, addresses map[string]deployment.TypeAndVersion) (*StaticLinkTokenState, error) {
state := StaticLinkTokenState{}
staticLinkToken := deployment.NewTypeAndVersion(types.StaticLinkToken, deployment.Version1_0_0)
// Perhaps revisit if we have a use case for multiple.
_, err := deployment.AddressesContainBundle(addresses, map[deployment.TypeAndVersion]struct{}{staticLinkToken: {}})
if err != nil {
return nil, fmt.Errorf("unable to check static link token on chain %s error: %w", chain.Name(), err)
}
for address, tvStr := range addresses {
switch tvStr {
case staticLinkToken:
lt, err := link_token_interface.NewLinkToken(common.HexToAddress(address), chain.Client)
if err != nil {
return nil, err
}
state.StaticLinkToken = lt
}
}
return &state, nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ func TestTransferToMCMSWithTimelock(t *testing.T) {
require.NoError(t, err)
addrs, err := e.ExistingAddresses.AddressesForChain(chain1)
require.NoError(t, err)
state, err := LoadMCMSWithTimelockState(e.Chains[chain1], addrs)
state, err := MaybeLoadMCMSWithTimelockState(e.Chains[chain1], addrs)
require.NoError(t, err)
link, err := LoadLinkTokenState(e.Chains[chain1], addrs)
link, err := MaybeLoadLinkTokenState(e.Chains[chain1], addrs)
require.NoError(t, err)
e, err = ApplyChangesets(t, e, map[uint64]*owner_helpers.RBACTimelock{
chain1: state.Timelock,
Expand All @@ -61,7 +61,7 @@ func TestTransferToMCMSWithTimelock(t *testing.T) {
})
require.NoError(t, err)
// We expect now that the link token is owned by the MCMS timelock.
link, err = LoadLinkTokenState(e.Chains[chain1], addrs)
link, err = MaybeLoadLinkTokenState(e.Chains[chain1], addrs)
require.NoError(t, err)
o, err := link.LinkToken.Owner(nil)
require.NoError(t, err)
Expand Down
Loading

0 comments on commit 4a0e8f0

Please sign in to comment.