Skip to content

Commit ab31fc0

Browse files
authored
Add AcceptOwnershipForwarder changeset implementation and tests (#22148)
* Add AcceptOwnershipForwarder implementation and tests * Remove unused imports in accept_ownership.go * Fix lint * Fix lint * Update accept_ownership_test to include new owner onboarding logic
1 parent 0a7d73f commit ab31fc0

2 files changed

Lines changed: 195 additions & 0 deletions

File tree

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package forwarder
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/ethereum/go-ethereum/common"
7+
8+
"github.com/smartcontractkit/chainlink-deployments-framework/datastore"
9+
cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
10+
forwarderwrapper "github.com/smartcontractkit/chainlink-evm/gethwrappers/keystone/generated/forwarder_1_0_0"
11+
12+
"github.com/smartcontractkit/chainlink/deployment/cre/contracts"
13+
)
14+
15+
// AcceptOwnershipInput identifies which KeystoneForwarder contract should accept pending ownership.
16+
type AcceptOwnershipInput struct {
17+
// ChainSelector of the chain where the forwarder is deployed.
18+
ChainSelector uint64 `json:"chainSelector" yaml:"chainSelector"`
19+
// Qualifier optionally disambiguates the forwarder in the datastore.
20+
// Leave empty if there is only one forwarder on the chain.
21+
Qualifier string `json:"qualifier,omitempty" yaml:"qualifier,omitempty"`
22+
}
23+
24+
// AcceptOwnershipForwarder directly calls AcceptOwnership on a KeystoneForwarder contract
25+
// using the environment's deployer key. Use this after the previous owner has called
26+
// transferOwnership to the deployer EOA.
27+
type AcceptOwnershipForwarder struct{}
28+
29+
var _ cldf.ChangeSetV2[AcceptOwnershipInput] = AcceptOwnershipForwarder{}
30+
31+
func (AcceptOwnershipForwarder) VerifyPreconditions(e cldf.Environment, input AcceptOwnershipInput) error {
32+
if _, ok := e.BlockChains.EVMChains()[input.ChainSelector]; !ok {
33+
return fmt.Errorf("chain selector %d not found in environment", input.ChainSelector)
34+
}
35+
filters := []datastore.FilterFunc[datastore.AddressRefKey, datastore.AddressRef]{
36+
datastore.AddressRefByChainSelector(input.ChainSelector),
37+
datastore.AddressRefByType(datastore.ContractType(contracts.KeystoneForwarder)),
38+
}
39+
if input.Qualifier != "" {
40+
filters = append(filters, datastore.AddressRefByQualifier(input.Qualifier))
41+
}
42+
refs := e.DataStore.Addresses().Filter(filters...)
43+
if len(refs) == 0 {
44+
return fmt.Errorf("no KeystoneForwarder found for chain %d (qualifier %q)", input.ChainSelector, input.Qualifier)
45+
}
46+
return nil
47+
}
48+
49+
func (AcceptOwnershipForwarder) Apply(e cldf.Environment, input AcceptOwnershipInput) (cldf.ChangesetOutput, error) {
50+
chain := e.BlockChains.EVMChains()[input.ChainSelector]
51+
52+
filters := []datastore.FilterFunc[datastore.AddressRefKey, datastore.AddressRef]{
53+
datastore.AddressRefByChainSelector(input.ChainSelector),
54+
datastore.AddressRefByType(datastore.ContractType(contracts.KeystoneForwarder)),
55+
}
56+
if input.Qualifier != "" {
57+
filters = append(filters, datastore.AddressRefByQualifier(input.Qualifier))
58+
}
59+
60+
refs := e.DataStore.Addresses().Filter(filters...)
61+
for _, ref := range refs {
62+
contract, err := forwarderwrapper.NewKeystoneForwarder(common.HexToAddress(ref.Address), chain.Client)
63+
if err != nil {
64+
return cldf.ChangesetOutput{}, fmt.Errorf("failed to instantiate forwarder %s on chain %d: %w", ref.Address, input.ChainSelector, err)
65+
}
66+
67+
tx, err := contract.AcceptOwnership(chain.DeployerKey)
68+
if _, confErr := cldf.ConfirmIfNoError(chain, tx, err); confErr != nil {
69+
return cldf.ChangesetOutput{}, fmt.Errorf("AcceptOwnership failed for %s on chain %d: %w", ref.Address, input.ChainSelector, confErr)
70+
}
71+
72+
e.Logger.Infow("Accepted ownership of KeystoneForwarder", "address", ref.Address, "chainSelector", input.ChainSelector)
73+
}
74+
75+
return cldf.ChangesetOutput{}, nil
76+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package forwarder_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/ethereum/go-ethereum/common"
7+
chainsel "github.com/smartcontractkit/chain-selectors"
8+
"github.com/stretchr/testify/require"
9+
10+
"github.com/smartcontractkit/chainlink-common/pkg/logger"
11+
cldfchain "github.com/smartcontractkit/chainlink-deployments-framework/chain"
12+
"github.com/smartcontractkit/chainlink-deployments-framework/datastore"
13+
cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
14+
"github.com/smartcontractkit/chainlink-deployments-framework/engine/test/environment"
15+
"github.com/smartcontractkit/chainlink-deployments-framework/engine/test/onchain"
16+
"github.com/smartcontractkit/chainlink-deployments-framework/operations"
17+
18+
forwarderwrapper "github.com/smartcontractkit/chainlink-evm/gethwrappers/keystone/generated/forwarder_1_0_0"
19+
20+
"github.com/smartcontractkit/chainlink/deployment/cre/contracts"
21+
"github.com/smartcontractkit/chainlink/deployment/cre/forwarder"
22+
)
23+
24+
func TestAcceptOwnershipForwarder(t *testing.T) {
25+
t.Parallel()
26+
27+
selector := chainsel.TEST_90000001.Selector
28+
29+
// One additional funded account acts as the "new owner" that will accept ownership,
30+
// mirroring the real scenario where the previous owner transfers to the deployer EOA.
31+
env, err := environment.New(t.Context(),
32+
environment.WithEVMSimulatedWithConfig(t, []uint64{selector}, onchain.EVMSimLoaderConfig{
33+
NumAdditionalAccounts: 1,
34+
}),
35+
environment.WithLogger(logger.Test(t)),
36+
)
37+
require.NoError(t, err)
38+
39+
// Deploy a KeystoneForwarder — deployer becomes the initial owner.
40+
deployOut, err := operations.ExecuteSequence(env.OperationsBundle, forwarder.DeploySequence,
41+
forwarder.DeploySequenceDeps{Env: env},
42+
forwarder.DeploySequenceInput{
43+
Targets: []uint64{selector},
44+
Qualifier: "test-accept-ownership",
45+
},
46+
)
47+
require.NoError(t, err)
48+
49+
env.DataStore = deployOut.Output.Datastore
50+
51+
refs := env.DataStore.Addresses().Filter(
52+
datastore.AddressRefByChainSelector(selector),
53+
datastore.AddressRefByType(datastore.ContractType(contracts.KeystoneForwarder)),
54+
)
55+
require.Len(t, refs, 1)
56+
57+
chain := env.BlockChains.EVMChains()[selector]
58+
require.NotEmpty(t, chain.Users, "expected at least one additional funded account")
59+
60+
// Users[0] is the new owner — the deployer (current owner) transfers to it.
61+
newOwner := chain.Users[0]
62+
63+
contract, err := forwarderwrapper.NewKeystoneForwarder(common.HexToAddress(refs[0].Address), chain.Client)
64+
require.NoError(t, err)
65+
66+
tx, err := contract.TransferOwnership(chain.DeployerKey, newOwner.From)
67+
_, err = cldf.ConfirmIfNoError(chain, tx, err)
68+
require.NoError(t, err)
69+
70+
// Rebuild the environment's BlockChains with newOwner as the deployer key
71+
// so the changeset signs acceptOwnership as the pending owner.
72+
chain.DeployerKey = newOwner
73+
evmChains := env.BlockChains.EVMChains()
74+
evmChains[selector] = chain
75+
blockChainMap := make(map[uint64]cldfchain.BlockChain, len(evmChains))
76+
for k, v := range evmChains {
77+
blockChainMap[k] = v
78+
}
79+
env.BlockChains = cldfchain.NewBlockChains(blockChainMap)
80+
81+
// Apply the changeset — new owner accepts the pending ownership transfer.
82+
_, err = forwarder.AcceptOwnershipForwarder{}.Apply(*env, forwarder.AcceptOwnershipInput{
83+
ChainSelector: selector,
84+
Qualifier: "test-accept-ownership",
85+
})
86+
require.NoError(t, err)
87+
88+
owner, err := contract.Owner(nil)
89+
require.NoError(t, err)
90+
require.Equal(t, newOwner.From, owner)
91+
}
92+
93+
func TestAcceptOwnershipForwarder_VerifyPreconditions(t *testing.T) {
94+
t.Parallel()
95+
96+
selector := chainsel.TEST_90000001.Selector
97+
98+
env, err := environment.New(t.Context(),
99+
environment.WithEVMSimulated(t, []uint64{selector}),
100+
environment.WithLogger(logger.Test(t)),
101+
)
102+
require.NoError(t, err)
103+
104+
t.Run("unknown chain selector", func(t *testing.T) {
105+
err := forwarder.AcceptOwnershipForwarder{}.VerifyPreconditions(*env, forwarder.AcceptOwnershipInput{
106+
ChainSelector: 0,
107+
})
108+
require.Error(t, err)
109+
require.Contains(t, err.Error(), "not found in environment")
110+
})
111+
112+
t.Run("no forwarder in datastore", func(t *testing.T) {
113+
err := forwarder.AcceptOwnershipForwarder{}.VerifyPreconditions(*env, forwarder.AcceptOwnershipInput{
114+
ChainSelector: selector,
115+
})
116+
require.Error(t, err)
117+
require.Contains(t, err.Error(), "no KeystoneForwarder found")
118+
})
119+
}

0 commit comments

Comments
 (0)