-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add example changeset to transfer from timelock signer account (#…
…16476) * feat: add changeset to transfer from timelock signer account * fix: linting errors * fix: code cleanup * fix: use NewTransactionFromInstruction * fix: cleanup accounts signer set
- Loading branch information
1 parent
9bee27e
commit b17c12a
Showing
3 changed files
with
394 additions
and
7 deletions.
There are no files selected for viewing
136 changes: 136 additions & 0 deletions
136
deployment/common/changeset/example/solana_transfer_mcm.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
package example | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"time" | ||
|
||
"github.com/gagliardetto/solana-go" | ||
"github.com/gagliardetto/solana-go/rpc" | ||
"github.com/smartcontractkit/mcms" | ||
mcmssolanasdk "github.com/smartcontractkit/mcms/sdk/solana" | ||
"github.com/smartcontractkit/mcms/types" | ||
|
||
"github.com/smartcontractkit/chainlink/deployment" | ||
solanachangeset "github.com/smartcontractkit/chainlink/deployment/common/changeset/solana" | ||
"github.com/smartcontractkit/chainlink/deployment/common/changeset/state" | ||
"github.com/smartcontractkit/chainlink/deployment/common/proposalutils" | ||
) | ||
|
||
var _ deployment.ChangeSetV2[TransferFromTimelockConfig] = TransferFromTimelock{} | ||
|
||
type TransferData struct { | ||
To solana.PublicKey | ||
Amount uint64 | ||
} | ||
type TransferFromTimelockConfig struct { | ||
TimelockDelay time.Duration | ||
AmountsPerChain map[uint64]TransferData | ||
} | ||
|
||
// TransferFromTimelock is a changeset that transfer funds from the timelock signer PDA | ||
// to the address provided in the config. It will return an mcms proposal to sign containing | ||
// the funds transfer transaction. | ||
type TransferFromTimelock struct{} | ||
|
||
// VerifyPreconditions checks if the deployer has enough SOL to fund the MCMS signers on each chain. | ||
func (f TransferFromTimelock) VerifyPreconditions(e deployment.Environment, config TransferFromTimelockConfig) error { | ||
// the number of accounts to fund per chain (bypasser, canceller, proposer, timelock) | ||
for chainSelector, amountCfg := range config.AmountsPerChain { | ||
solChain, ok := e.SolChains[chainSelector] | ||
if !ok { | ||
return fmt.Errorf("solana chain not found for selector %d", chainSelector) | ||
} | ||
if amountCfg.To.IsZero() { | ||
return errors.New("destination address is empty") | ||
} | ||
addresses, err := e.ExistingAddresses.AddressesForChain(chainSelector) | ||
if err != nil { | ||
return fmt.Errorf("failed to get existing addresses: %w", err) | ||
} | ||
mcmState, err := state.MaybeLoadMCMSWithTimelockChainStateSolana(solChain, addresses) | ||
if err != nil { | ||
return fmt.Errorf("failed to load MCMS state: %w", err) | ||
} | ||
// Check if seeds are empty | ||
if mcmState.TimelockSeed == [32]byte{} { | ||
return errors.New("timelock seeds are empty, please deploy MCMS contracts first") | ||
} | ||
// Check if program IDs exists | ||
if mcmState.TimelockProgram.IsZero() { | ||
return errors.New("timelock program IDs are empty, please deploy timelock program first") | ||
} | ||
result, err := solChain.Client.GetBalance(e.GetContext(), solChain.DeployerKey.PublicKey(), rpc.CommitmentConfirmed) | ||
if err != nil { | ||
return fmt.Errorf("failed to get deployer balance: %w", err) | ||
} | ||
if result.Value < amountCfg.Amount { | ||
return fmt.Errorf("deployer balance is insufficient, required: %d, actual: %d", amountCfg.Amount, result.Value) | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
// Apply funds the MCMS signers on each chain. | ||
func (f TransferFromTimelock) Apply(e deployment.Environment, config TransferFromTimelockConfig) (deployment.ChangesetOutput, error) { | ||
timelocks := map[uint64]string{} | ||
proposers := map[uint64]string{} | ||
var batches []types.BatchOperation | ||
inspectors, err := proposalutils.McmsInspectors(e) | ||
if err != nil { | ||
return deployment.ChangesetOutput{}, fmt.Errorf("failed to get MCMS inspectors: %w", err) | ||
} | ||
for chainSelector, cfgAmounts := range config.AmountsPerChain { | ||
solChain := e.SolChains[chainSelector] | ||
addreses, err := e.ExistingAddresses.AddressesForChain(chainSelector) | ||
if err != nil { | ||
return deployment.ChangesetOutput{}, fmt.Errorf("failed to get existing addresses: %w", err) | ||
} | ||
mcmState, err := state.MaybeLoadMCMSWithTimelockChainStateSolana(solChain, addreses) | ||
if err != nil { | ||
return deployment.ChangesetOutput{}, fmt.Errorf("failed to load MCMS state: %w", err) | ||
} | ||
timelockSignerPDA := state.GetTimelockSignerPDA(mcmState.TimelockProgram, mcmState.TimelockSeed) | ||
timelockID := mcmssolanasdk.ContractAddress(mcmState.TimelockProgram, mcmssolanasdk.PDASeed(mcmState.TimelockSeed)) | ||
proposerID := mcmssolanasdk.ContractAddress(mcmState.McmProgram, mcmssolanasdk.PDASeed(mcmState.ProposerMcmSeed)) | ||
timelocks[chainSelector] = timelockID | ||
proposers[chainSelector] = proposerID | ||
ixs, err := solanachangeset.FundFromAddressIxs( | ||
solChain, | ||
timelockSignerPDA, | ||
[]solana.PublicKey{cfgAmounts.To}, | ||
cfgAmounts.Amount) | ||
if err != nil { | ||
return deployment.ChangesetOutput{}, fmt.Errorf("failed to fund timelock signer on chain %d: %w", chainSelector, err) | ||
} | ||
|
||
var transactions []types.Transaction | ||
|
||
for _, ix := range ixs { | ||
solanaTx, err := mcmssolanasdk.NewTransactionFromInstruction(ix, "SystemProgram", []string{}) | ||
if err != nil { | ||
return deployment.ChangesetOutput{}, fmt.Errorf("failed to create transaction: %w", err) | ||
} | ||
transactions = append(transactions, solanaTx) | ||
} | ||
batches = append(batches, types.BatchOperation{ | ||
ChainSelector: types.ChainSelector(chainSelector), | ||
Transactions: transactions, | ||
}) | ||
} | ||
proposal, err := proposalutils.BuildProposalFromBatchesV2( | ||
e.GetContext(), | ||
timelocks, | ||
proposers, | ||
inspectors, | ||
batches, | ||
"transfer funds from timelock singer", | ||
config.TimelockDelay, | ||
) | ||
if err != nil { | ||
return deployment.ChangesetOutput{}, fmt.Errorf("failed to build proposal: %w", err) | ||
} | ||
return deployment.ChangesetOutput{ | ||
MCMSTimelockProposals: []mcms.TimelockProposal{*proposal}, | ||
}, nil | ||
} |
242 changes: 242 additions & 0 deletions
242
deployment/common/changeset/example/solana_transfer_mcm_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,242 @@ | ||
package example_test | ||
|
||
import ( | ||
"fmt" | ||
"testing" | ||
"time" | ||
|
||
"github.com/gagliardetto/solana-go" | ||
"github.com/gagliardetto/solana-go/rpc" | ||
chainselectors "github.com/smartcontractkit/chain-selectors" | ||
mcmsSolana "github.com/smartcontractkit/mcms/sdk/solana" | ||
"github.com/stretchr/testify/require" | ||
"go.uber.org/zap/zapcore" | ||
|
||
"github.com/smartcontractkit/chainlink/deployment" | ||
"github.com/smartcontractkit/chainlink/deployment/ccip/changeset/testhelpers" | ||
"github.com/smartcontractkit/chainlink/deployment/common/changeset" | ||
"github.com/smartcontractkit/chainlink/deployment/common/changeset/example" | ||
"github.com/smartcontractkit/chainlink/deployment/common/changeset/state" | ||
"github.com/smartcontractkit/chainlink/deployment/common/proposalutils" | ||
"github.com/smartcontractkit/chainlink/deployment/common/types" | ||
"github.com/smartcontractkit/chainlink/deployment/environment/memory" | ||
"github.com/smartcontractkit/chainlink/v2/core/logger" | ||
) | ||
|
||
// setupFundingTestEnv deploys all required contracts for the funding test | ||
func setupFundingTestEnv(t *testing.T) deployment.Environment { | ||
lggr := logger.TestLogger(t) | ||
cfg := memory.MemoryEnvironmentConfig{ | ||
SolChains: 1, | ||
} | ||
env := memory.NewMemoryEnvironment(t, lggr, zapcore.DebugLevel, cfg) | ||
chainSelector := env.AllChainSelectorsSolana()[0] | ||
|
||
config := proposalutils.SingleGroupTimelockConfigV2(t) | ||
testhelpers.SavePreloadedSolAddresses(t, env, chainSelector) | ||
// Initialize the address book with a dummy address to avoid deploy precondition errors. | ||
err := env.ExistingAddresses.Save(chainSelector, "dummyAddress", deployment.TypeAndVersion{Type: "dummy", Version: deployment.Version1_0_0}) | ||
require.NoError(t, err) | ||
|
||
// Deploy MCMS and Timelock | ||
env, err = changeset.Apply(t, env, nil, | ||
changeset.Configure( | ||
deployment.CreateLegacyChangeSet(changeset.DeployMCMSWithTimelockV2), | ||
map[uint64]types.MCMSWithTimelockConfigV2{ | ||
chainSelector: config, | ||
}, | ||
), | ||
) | ||
require.NoError(t, err) | ||
|
||
return env | ||
} | ||
|
||
func TestTransferFromTimelockConfig_VerifyPreconditions(t *testing.T) { | ||
lggr := logger.TestLogger(t) | ||
validEnv := memory.NewMemoryEnvironment(t, lggr, zapcore.InfoLevel, memory.MemoryEnvironmentConfig{SolChains: 1}) | ||
validEnv.SolChains[chainselectors.SOLANA_DEVNET.Selector] = deployment.SolChain{} | ||
validSolChainSelector := validEnv.AllChainSelectorsSolana()[0] | ||
receiverKey := solana.NewWallet().PublicKey() | ||
cs := example.TransferFromTimelock{} | ||
timelockID := mcmsSolana.ContractAddress( | ||
solana.NewWallet().PublicKey(), | ||
[32]byte{'t', 'e', 's', 't'}, | ||
) | ||
err := validEnv.ExistingAddresses.Save(validSolChainSelector, timelockID, deployment.TypeAndVersion{ | ||
Type: types.RBACTimelock, | ||
Version: deployment.Version1_0_0, | ||
}) | ||
require.NoError(t, err) | ||
|
||
// Create an environment that simulates a chain where the MCMS contracts have not been deployed, | ||
// e.g. missing the required addresses so that the state loader returns empty seeds. | ||
noTimelockEnv := memory.NewMemoryEnvironment(t, lggr, zapcore.InfoLevel, memory.MemoryEnvironmentConfig{ | ||
SolChains: 1, | ||
}) | ||
noTimelockEnv.SolChains[chainselectors.SOLANA_DEVNET.Selector] = deployment.SolChain{} | ||
err = noTimelockEnv.ExistingAddresses.Save(chainselectors.SOLANA_DEVNET.Selector, "dummy", deployment.TypeAndVersion{ | ||
Type: "Sometype", | ||
Version: deployment.Version1_0_0, | ||
}) | ||
require.NoError(t, err) | ||
|
||
// Create an environment with a Solana chain that has an invalid (zero) underlying chain. | ||
invalidSolChainEnv := memory.NewMemoryEnvironment(t, lggr, zapcore.InfoLevel, memory.MemoryEnvironmentConfig{ | ||
SolChains: 0, | ||
}) | ||
invalidSolChainEnv.SolChains[validSolChainSelector] = deployment.SolChain{} | ||
|
||
tests := []struct { | ||
name string | ||
env deployment.Environment | ||
config example.TransferFromTimelockConfig | ||
expectedError string | ||
}{ | ||
{ | ||
name: "All preconditions satisfied", | ||
env: validEnv, | ||
config: example.TransferFromTimelockConfig{ | ||
AmountsPerChain: map[uint64]example.TransferData{validSolChainSelector: { | ||
Amount: 100, | ||
To: receiverKey, | ||
}}, | ||
}, | ||
expectedError: "", | ||
}, | ||
{ | ||
name: "No Solana chains found in environment", | ||
env: memory.NewMemoryEnvironment(t, lggr, zapcore.InfoLevel, memory.MemoryEnvironmentConfig{ | ||
Bootstraps: 1, | ||
Chains: 1, | ||
SolChains: 0, | ||
Nodes: 1, | ||
}), | ||
config: example.TransferFromTimelockConfig{ | ||
AmountsPerChain: map[uint64]example.TransferData{validSolChainSelector: { | ||
Amount: 100, | ||
To: receiverKey, | ||
}}, | ||
}, | ||
expectedError: fmt.Sprintf("solana chain not found for selector %d", validSolChainSelector), | ||
}, | ||
{ | ||
name: "Chain selector not found in environment", | ||
env: validEnv, | ||
config: example.TransferFromTimelockConfig{AmountsPerChain: map[uint64]example.TransferData{99999: { | ||
Amount: 100, | ||
To: receiverKey, | ||
}}}, | ||
expectedError: "solana chain not found for selector 99999", | ||
}, | ||
{ | ||
name: "timelock contracts not deployed (empty seeds)", | ||
env: noTimelockEnv, | ||
config: example.TransferFromTimelockConfig{ | ||
AmountsPerChain: map[uint64]example.TransferData{chainselectors.SOLANA_DEVNET.Selector: { | ||
Amount: 100, | ||
To: receiverKey, | ||
}}, | ||
}, | ||
expectedError: "timelock seeds are empty, please deploy MCMS contracts first", | ||
}, | ||
{ | ||
name: "Insufficient deployer balance", | ||
env: validEnv, | ||
config: example.TransferFromTimelockConfig{ | ||
AmountsPerChain: map[uint64]example.TransferData{ | ||
validSolChainSelector: { | ||
Amount: 999999999999999999, | ||
To: receiverKey, | ||
}, | ||
}, | ||
}, | ||
expectedError: "deployer balance is insufficient", | ||
}, | ||
{ | ||
name: "Insufficient deployer balance", | ||
env: validEnv, | ||
config: example.TransferFromTimelockConfig{ | ||
AmountsPerChain: map[uint64]example.TransferData{ | ||
validSolChainSelector: { | ||
Amount: 999999999999999999, | ||
To: receiverKey, | ||
}, | ||
}, | ||
}, | ||
expectedError: "deployer balance is insufficient", | ||
}, | ||
{ | ||
name: "Invalid Solana chain in environment", | ||
env: invalidSolChainEnv, | ||
config: example.TransferFromTimelockConfig{ | ||
AmountsPerChain: map[uint64]example.TransferData{validSolChainSelector: { | ||
Amount: 100, | ||
To: receiverKey, | ||
}}, | ||
}, | ||
expectedError: "failed to get existing addresses: chain selector 12463857294658392847: chain not found", | ||
}, | ||
{ | ||
name: "empty from field", | ||
env: invalidSolChainEnv, | ||
config: example.TransferFromTimelockConfig{ | ||
AmountsPerChain: map[uint64]example.TransferData{validSolChainSelector: { | ||
Amount: 100, | ||
To: solana.PublicKey{}, | ||
}}, | ||
}, | ||
expectedError: "destination address is empty", | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
err := cs.VerifyPreconditions(tt.env, tt.config) | ||
if tt.expectedError == "" { | ||
require.NoError(t, err) | ||
} else { | ||
require.Error(t, err) | ||
require.Contains(t, err.Error(), tt.expectedError) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestTransferFromTimelockConfig_Apply(t *testing.T) { | ||
env := setupFundingTestEnv(t) | ||
cfgAmounts := example.TransferData{ | ||
Amount: 100 * solana.LAMPORTS_PER_SOL, | ||
To: solana.NewWallet().PublicKey(), | ||
} | ||
amountsPerChain := make(map[uint64]example.TransferData) | ||
for chainSelector := range env.SolChains { | ||
amountsPerChain[chainSelector] = cfgAmounts | ||
} | ||
config := example.TransferFromTimelockConfig{ | ||
TimelockDelay: 1 * time.Second, | ||
AmountsPerChain: amountsPerChain, | ||
} | ||
addresses, err := env.ExistingAddresses.AddressesForChain(env.AllChainSelectorsSolana()[0]) | ||
require.NoError(t, err) | ||
mcmState, err := state.MaybeLoadMCMSWithTimelockChainStateSolana(env.SolChains[env.AllChainSelectorsSolana()[0]], addresses) | ||
require.NoError(t, err) | ||
timelockSigner := state.GetTimelockSignerPDA(mcmState.TimelockProgram, mcmState.TimelockSeed) | ||
mcmSigner := state.GetMCMSignerPDA(mcmState.McmProgram, mcmState.ProposerMcmSeed) | ||
chainSelector := env.AllChainSelectorsSolana()[0] | ||
solChain := env.SolChains[chainSelector] | ||
memory.FundSolanaAccounts(env.GetContext(), t, []solana.PublicKey{timelockSigner, mcmSigner, solChain.DeployerKey.PublicKey()}, 150, solChain.Client) | ||
|
||
changesetInstance := example.TransferFromTimelock{} | ||
|
||
env, err = changeset.ApplyChangesetsV2(t, env, []changeset.ConfiguredChangeSet{ | ||
changeset.Configure(changesetInstance, config), | ||
}) | ||
require.NoError(t, err) | ||
|
||
balance, err := solChain.Client.GetBalance(env.GetContext(), cfgAmounts.To, rpc.CommitmentConfirmed) | ||
require.NoError(t, err) | ||
t.Logf("Account: %s, Balance: %d", cfgAmounts.To, balance.Value) | ||
|
||
require.Equal(t, cfgAmounts.Amount, balance.Value) | ||
} |
Oops, something went wrong.