diff --git a/contracts/generated/ocr2/Close.go b/contracts/generated/ocr2/Close.go new file mode 100644 index 000000000..08cf0c939 --- /dev/null +++ b/contracts/generated/ocr2/Close.go @@ -0,0 +1,136 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. + +package ocr_2 + +import ( + "errors" + ag_binary "github.com/gagliardetto/binary" + ag_solanago "github.com/gagliardetto/solana-go" + ag_format "github.com/gagliardetto/solana-go/text/format" + ag_treeout "github.com/gagliardetto/treeout" +) + +// Close is the `close` instruction. +type Close struct { + + // [0] = [WRITE] state + // + // [1] = [WRITE] receiver + // + // [2] = [SIGNER] authority + ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +// NewCloseInstructionBuilder creates a new `Close` instruction builder. +func NewCloseInstructionBuilder() *Close { + nd := &Close{ + AccountMetaSlice: make(ag_solanago.AccountMetaSlice, 3), + } + return nd +} + +// SetStateAccount sets the "state" account. +func (inst *Close) SetStateAccount(state ag_solanago.PublicKey) *Close { + inst.AccountMetaSlice[0] = ag_solanago.Meta(state).WRITE() + return inst +} + +// GetStateAccount gets the "state" account. +func (inst *Close) GetStateAccount() *ag_solanago.AccountMeta { + return inst.AccountMetaSlice[0] +} + +// SetReceiverAccount sets the "receiver" account. +func (inst *Close) SetReceiverAccount(receiver ag_solanago.PublicKey) *Close { + inst.AccountMetaSlice[1] = ag_solanago.Meta(receiver).WRITE() + return inst +} + +// GetReceiverAccount gets the "receiver" account. +func (inst *Close) GetReceiverAccount() *ag_solanago.AccountMeta { + return inst.AccountMetaSlice[1] +} + +// SetAuthorityAccount sets the "authority" account. +func (inst *Close) SetAuthorityAccount(authority ag_solanago.PublicKey) *Close { + inst.AccountMetaSlice[2] = ag_solanago.Meta(authority).SIGNER() + return inst +} + +// GetAuthorityAccount gets the "authority" account. +func (inst *Close) GetAuthorityAccount() *ag_solanago.AccountMeta { + return inst.AccountMetaSlice[2] +} + +func (inst Close) Build() *Instruction { + return &Instruction{BaseVariant: ag_binary.BaseVariant{ + Impl: inst, + TypeID: Instruction_Close, + }} +} + +// ValidateAndBuild validates the instruction parameters and accounts; +// if there is a validation error, it returns the error. +// Otherwise, it builds and returns the instruction. +func (inst Close) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *Close) Validate() error { + // Check whether all (required) accounts are set: + { + if inst.AccountMetaSlice[0] == nil { + return errors.New("accounts.State is not set") + } + if inst.AccountMetaSlice[1] == nil { + return errors.New("accounts.Receiver is not set") + } + if inst.AccountMetaSlice[2] == nil { + return errors.New("accounts.Authority is not set") + } + } + return nil +} + +func (inst *Close) EncodeToTree(parent ag_treeout.Branches) { + parent.Child(ag_format.Program(ProgramName, ProgramID)). + // + ParentFunc(func(programBranch ag_treeout.Branches) { + programBranch.Child(ag_format.Instruction("Close")). + // + ParentFunc(func(instructionBranch ag_treeout.Branches) { + + // Parameters of the instruction: + instructionBranch.Child("Params[len=0]").ParentFunc(func(paramsBranch ag_treeout.Branches) {}) + + // Accounts of the instruction: + instructionBranch.Child("Accounts[len=3]").ParentFunc(func(accountsBranch ag_treeout.Branches) { + accountsBranch.Child(ag_format.Meta(" state", inst.AccountMetaSlice[0])) + accountsBranch.Child(ag_format.Meta(" receiver", inst.AccountMetaSlice[1])) + accountsBranch.Child(ag_format.Meta("authority", inst.AccountMetaSlice[2])) + }) + }) + }) +} + +func (obj Close) MarshalWithEncoder(encoder *ag_binary.Encoder) (err error) { + return nil +} +func (obj *Close) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (err error) { + return nil +} + +// NewCloseInstruction declares a new Close instruction with the provided parameters and accounts. +func NewCloseInstruction( + // Accounts: + state ag_solanago.PublicKey, + receiver ag_solanago.PublicKey, + authority ag_solanago.PublicKey) *Close { + return NewCloseInstructionBuilder(). + SetStateAccount(state). + SetReceiverAccount(receiver). + SetAuthorityAccount(authority) +} diff --git a/contracts/generated/ocr2/Close_test.go b/contracts/generated/ocr2/Close_test.go new file mode 100644 index 000000000..43c83073c --- /dev/null +++ b/contracts/generated/ocr2/Close_test.go @@ -0,0 +1,33 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. + +package ocr_2 + +import ( + "bytes" + ag_gofuzz "github.com/gagliardetto/gofuzz" + ag_require "github.com/stretchr/testify/require" + "strconv" + "testing" +) + +func TestEncodeDecode_Close(t *testing.T) { + fu := ag_gofuzz.New().NilChance(0) + for i := 0; i < 1; i++ { + t.Run("Close"+strconv.Itoa(i), func(t *testing.T) { + { + params := new(Close) + fu.Fuzz(params) + params.AccountMetaSlice = nil + buf := new(bytes.Buffer) + err := encodeT(*params, buf) + ag_require.NoError(t, err) + // + got := new(Close) + err = decodeT(got, buf.Bytes()) + got.AccountMetaSlice = nil + ag_require.NoError(t, err) + ag_require.Equal(t, params, got) + } + }) + } +} diff --git a/contracts/generated/ocr2/instructions.go b/contracts/generated/ocr2/instructions.go index 72eeae0c0..563e38c20 100644 --- a/contracts/generated/ocr2/instructions.go +++ b/contracts/generated/ocr2/instructions.go @@ -30,6 +30,8 @@ func init() { var ( Instruction_Initialize = ag_binary.TypeID([8]byte{175, 175, 109, 31, 13, 152, 155, 237}) + Instruction_Close = ag_binary.TypeID([8]byte{98, 165, 201, 177, 108, 65, 206, 96}) + Instruction_TransferOwnership = ag_binary.TypeID([8]byte{65, 177, 215, 73, 53, 45, 99, 47}) Instruction_AcceptOwnership = ag_binary.TypeID([8]byte{172, 23, 43, 13, 238, 213, 85, 150}) @@ -70,6 +72,8 @@ func InstructionIDToName(id ag_binary.TypeID) string { switch id { case Instruction_Initialize: return "Initialize" + case Instruction_Close: + return "Close" case Instruction_TransferOwnership: return "TransferOwnership" case Instruction_AcceptOwnership: @@ -127,6 +131,9 @@ var InstructionImplDef = ag_binary.NewVariantDefinition( { "initialize", (*Initialize)(nil), }, + { + "close", (*Close)(nil), + }, { "transfer_ownership", (*TransferOwnership)(nil), }, diff --git a/contracts/generated/store/CloseFeed.go b/contracts/generated/store/CloseFeed.go new file mode 100644 index 000000000..845a0ce88 --- /dev/null +++ b/contracts/generated/store/CloseFeed.go @@ -0,0 +1,155 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. + +package store + +import ( + "errors" + ag_binary "github.com/gagliardetto/binary" + ag_solanago "github.com/gagliardetto/solana-go" + ag_format "github.com/gagliardetto/solana-go/text/format" + ag_treeout "github.com/gagliardetto/treeout" +) + +// CloseFeed is the `closeFeed` instruction. +type CloseFeed struct { + + // [0] = [] store + // + // [1] = [WRITE] feed + // + // [2] = [WRITE] receiver + // + // [3] = [SIGNER] authority + ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +// NewCloseFeedInstructionBuilder creates a new `CloseFeed` instruction builder. +func NewCloseFeedInstructionBuilder() *CloseFeed { + nd := &CloseFeed{ + AccountMetaSlice: make(ag_solanago.AccountMetaSlice, 4), + } + return nd +} + +// SetStoreAccount sets the "store" account. +func (inst *CloseFeed) SetStoreAccount(store ag_solanago.PublicKey) *CloseFeed { + inst.AccountMetaSlice[0] = ag_solanago.Meta(store) + return inst +} + +// GetStoreAccount gets the "store" account. +func (inst *CloseFeed) GetStoreAccount() *ag_solanago.AccountMeta { + return inst.AccountMetaSlice[0] +} + +// SetFeedAccount sets the "feed" account. +func (inst *CloseFeed) SetFeedAccount(feed ag_solanago.PublicKey) *CloseFeed { + inst.AccountMetaSlice[1] = ag_solanago.Meta(feed).WRITE() + return inst +} + +// GetFeedAccount gets the "feed" account. +func (inst *CloseFeed) GetFeedAccount() *ag_solanago.AccountMeta { + return inst.AccountMetaSlice[1] +} + +// SetReceiverAccount sets the "receiver" account. +func (inst *CloseFeed) SetReceiverAccount(receiver ag_solanago.PublicKey) *CloseFeed { + inst.AccountMetaSlice[2] = ag_solanago.Meta(receiver).WRITE() + return inst +} + +// GetReceiverAccount gets the "receiver" account. +func (inst *CloseFeed) GetReceiverAccount() *ag_solanago.AccountMeta { + return inst.AccountMetaSlice[2] +} + +// SetAuthorityAccount sets the "authority" account. +func (inst *CloseFeed) SetAuthorityAccount(authority ag_solanago.PublicKey) *CloseFeed { + inst.AccountMetaSlice[3] = ag_solanago.Meta(authority).SIGNER() + return inst +} + +// GetAuthorityAccount gets the "authority" account. +func (inst *CloseFeed) GetAuthorityAccount() *ag_solanago.AccountMeta { + return inst.AccountMetaSlice[3] +} + +func (inst CloseFeed) Build() *Instruction { + return &Instruction{BaseVariant: ag_binary.BaseVariant{ + Impl: inst, + TypeID: Instruction_CloseFeed, + }} +} + +// ValidateAndBuild validates the instruction parameters and accounts; +// if there is a validation error, it returns the error. +// Otherwise, it builds and returns the instruction. +func (inst CloseFeed) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *CloseFeed) Validate() error { + // Check whether all (required) accounts are set: + { + if inst.AccountMetaSlice[0] == nil { + return errors.New("accounts.Store is not set") + } + if inst.AccountMetaSlice[1] == nil { + return errors.New("accounts.Feed is not set") + } + if inst.AccountMetaSlice[2] == nil { + return errors.New("accounts.Receiver is not set") + } + if inst.AccountMetaSlice[3] == nil { + return errors.New("accounts.Authority is not set") + } + } + return nil +} + +func (inst *CloseFeed) EncodeToTree(parent ag_treeout.Branches) { + parent.Child(ag_format.Program(ProgramName, ProgramID)). + // + ParentFunc(func(programBranch ag_treeout.Branches) { + programBranch.Child(ag_format.Instruction("CloseFeed")). + // + ParentFunc(func(instructionBranch ag_treeout.Branches) { + + // Parameters of the instruction: + instructionBranch.Child("Params[len=0]").ParentFunc(func(paramsBranch ag_treeout.Branches) {}) + + // Accounts of the instruction: + instructionBranch.Child("Accounts[len=4]").ParentFunc(func(accountsBranch ag_treeout.Branches) { + accountsBranch.Child(ag_format.Meta(" store", inst.AccountMetaSlice[0])) + accountsBranch.Child(ag_format.Meta(" feed", inst.AccountMetaSlice[1])) + accountsBranch.Child(ag_format.Meta(" receiver", inst.AccountMetaSlice[2])) + accountsBranch.Child(ag_format.Meta("authority", inst.AccountMetaSlice[3])) + }) + }) + }) +} + +func (obj CloseFeed) MarshalWithEncoder(encoder *ag_binary.Encoder) (err error) { + return nil +} +func (obj *CloseFeed) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (err error) { + return nil +} + +// NewCloseFeedInstruction declares a new CloseFeed instruction with the provided parameters and accounts. +func NewCloseFeedInstruction( + // Accounts: + store ag_solanago.PublicKey, + feed ag_solanago.PublicKey, + receiver ag_solanago.PublicKey, + authority ag_solanago.PublicKey) *CloseFeed { + return NewCloseFeedInstructionBuilder(). + SetStoreAccount(store). + SetFeedAccount(feed). + SetReceiverAccount(receiver). + SetAuthorityAccount(authority) +} diff --git a/contracts/generated/store/CloseFeed_test.go b/contracts/generated/store/CloseFeed_test.go new file mode 100644 index 000000000..961cab2b0 --- /dev/null +++ b/contracts/generated/store/CloseFeed_test.go @@ -0,0 +1,33 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. + +package store + +import ( + "bytes" + ag_gofuzz "github.com/gagliardetto/gofuzz" + ag_require "github.com/stretchr/testify/require" + "strconv" + "testing" +) + +func TestEncodeDecode_CloseFeed(t *testing.T) { + fu := ag_gofuzz.New().NilChance(0) + for i := 0; i < 1; i++ { + t.Run("CloseFeed"+strconv.Itoa(i), func(t *testing.T) { + { + params := new(CloseFeed) + fu.Fuzz(params) + params.AccountMetaSlice = nil + buf := new(bytes.Buffer) + err := encodeT(*params, buf) + ag_require.NoError(t, err) + // + got := new(CloseFeed) + err = decodeT(got, buf.Bytes()) + got.AccountMetaSlice = nil + ag_require.NoError(t, err) + ag_require.Equal(t, params, got) + } + }) + } +} diff --git a/contracts/generated/store/instructions.go b/contracts/generated/store/instructions.go index dd42f1f3c..e18fa5289 100644 --- a/contracts/generated/store/instructions.go +++ b/contracts/generated/store/instructions.go @@ -32,6 +32,8 @@ var ( Instruction_CreateFeed = ag_binary.TypeID([8]byte{173, 86, 95, 94, 13, 193, 67, 180}) + Instruction_CloseFeed = ag_binary.TypeID([8]byte{153, 14, 92, 89, 19, 78, 211, 46}) + Instruction_SetValidatorConfig = ag_binary.TypeID([8]byte{87, 248, 224, 193, 17, 41, 80, 250}) Instruction_SetWriter = ag_binary.TypeID([8]byte{174, 36, 177, 122, 86, 142, 32, 109}) @@ -56,6 +58,8 @@ func InstructionIDToName(id ag_binary.TypeID) string { return "Initialize" case Instruction_CreateFeed: return "CreateFeed" + case Instruction_CloseFeed: + return "CloseFeed" case Instruction_SetValidatorConfig: return "SetValidatorConfig" case Instruction_SetWriter: @@ -98,6 +102,9 @@ var InstructionImplDef = ag_binary.NewVariantDefinition( { "create_feed", (*CreateFeed)(nil), }, + { + "close_feed", (*CloseFeed)(nil), + }, { "set_validator_config", (*SetValidatorConfig)(nil), }, diff --git a/contracts/programs/access-controller/src/lib.rs b/contracts/programs/access-controller/src/lib.rs index 3fbe7b11b..d992f6bca 100644 --- a/contracts/programs/access-controller/src/lib.rs +++ b/contracts/programs/access-controller/src/lib.rs @@ -19,7 +19,7 @@ pub const MAX_ADDRS: usize = 64; #[zero_copy] pub struct AccessList { - xs: [Pubkey; 64], // sadly we can't use const https://github.com/project-serum/anchor/issues/632 + xs: [Pubkey; MAX_ADDRS], len: u64, } arrayvec!(AccessList, Pubkey, u64); @@ -30,12 +30,10 @@ const_assert!( #[account(zero_copy)] pub struct AccessController { pub owner: Pubkey, + pub proposed_owner: Pubkey, pub access_list: AccessList, } -// IDEA: use a PDA with seeds = [account()], bump = ? to check for proof that account exists -// the tradeoff would be that we would have to calculate the PDA and pass it as an account everywhere - #[program] pub mod access_controller { use super::*; @@ -45,6 +43,27 @@ pub mod access_controller { Ok(()) } + #[access_control(owner(&ctx.accounts.state, &ctx.accounts.authority))] + pub fn transfer_ownership( + ctx: Context, + proposed_owner: Pubkey, + ) -> ProgramResult { + require!(proposed_owner != Pubkey::default(), InvalidInput); + let state = &mut *ctx.accounts.state.load_mut()?; + state.proposed_owner = proposed_owner; + Ok(()) + } + + pub fn accept_ownership(ctx: Context) -> ProgramResult { + let state = &mut *ctx.accounts.state.load_mut()?; + require!( + ctx.accounts.authority.key == &state.proposed_owner, + Unauthorized + ); + state.owner = std::mem::take(&mut state.proposed_owner); + Ok(()) + } + #[access_control(owner(&ctx.accounts.state, &ctx.accounts.owner))] pub fn add_access(ctx: Context) -> ProgramResult { let mut state = ctx.accounts.state.load_mut()?; @@ -93,8 +112,11 @@ pub enum ErrorCode { #[msg("Unauthorized")] Unauthorized = 0, + #[msg("Invalid input")] + InvalidInput = 1, + #[msg("Access list is full")] - Full = 1, + Full = 2, } #[derive(Accounts)] @@ -111,6 +133,20 @@ pub struct Initialize<'info> { pub system_program: AccountInfo<'info>, } +#[derive(Accounts)] +pub struct TransferOwnership<'info> { + #[account(mut)] + pub state: AccountLoader<'info, AccessController>, + pub authority: Signer<'info>, +} + +#[derive(Accounts)] +pub struct AcceptOwnership<'info> { + #[account(mut)] + pub state: AccountLoader<'info, AccessController>, + pub authority: Signer<'info>, +} + #[derive(Accounts)] pub struct AddAccess<'info> { #[account(mut, has_one = owner)] diff --git a/gauntlet/packages/gauntlet-solana-contracts/artifacts/schemas/access_controller.json b/gauntlet/packages/gauntlet-solana-contracts/artifacts/schemas/access_controller.json index 3319a3d98..331211fc9 100644 --- a/gauntlet/packages/gauntlet-solana-contracts/artifacts/schemas/access_controller.json +++ b/gauntlet/packages/gauntlet-solana-contracts/artifacts/schemas/access_controller.json @@ -42,6 +42,43 @@ ], "args": [] }, + { + "name": "transferOwnership", + "accounts": [ + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "proposedOwner", + "type": "publicKey" + } + ] + }, + { + "name": "acceptOwnership", + "accounts": [ + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [] + }, { "name": "addAccess", "accounts": [ @@ -95,6 +132,10 @@ "name": "owner", "type": "publicKey" }, + { + "name": "proposedOwner", + "type": "publicKey" + }, { "name": "accessList", "type": { @@ -136,8 +177,13 @@ }, { "code": 6001, + "name": "InvalidInput", + "msg": "Invalid input" + }, + { + "code": 6002, "name": "Full", "msg": "Access list is full" } ] -} +} \ No newline at end of file diff --git a/gauntlet/packages/gauntlet-solana-contracts/artifacts/schemas/ocr2.json b/gauntlet/packages/gauntlet-solana-contracts/artifacts/schemas/ocr2.json index 3ab664792..e51f86962 100644 --- a/gauntlet/packages/gauntlet-solana-contracts/artifacts/schemas/ocr2.json +++ b/gauntlet/packages/gauntlet-solana-contracts/artifacts/schemas/ocr2.json @@ -95,6 +95,27 @@ } ] }, + { + "name": "close", + "accounts": [ + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "receiver", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [] + }, { "name": "transferOwnership", "accounts": [ @@ -1054,4 +1075,4 @@ "msg": "Oracle transmitter key not found" } ] -} +} \ No newline at end of file diff --git a/gauntlet/packages/gauntlet-solana-contracts/artifacts/schemas/store.json b/gauntlet/packages/gauntlet-solana-contracts/artifacts/schemas/store.json index ff4f04094..c0b9f9c40 100644 --- a/gauntlet/packages/gauntlet-solana-contracts/artifacts/schemas/store.json +++ b/gauntlet/packages/gauntlet-solana-contracts/artifacts/schemas/store.json @@ -61,6 +61,32 @@ } ] }, + { + "name": "closeFeed", + "accounts": [ + { + "name": "store", + "isMut": false, + "isSigner": false + }, + { + "name": "feed", + "isMut": true, + "isSigner": false + }, + { + "name": "receiver", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [] + }, { "name": "setValidatorConfig", "accounts": [ @@ -500,4 +526,4 @@ "name": "NotFound" } ] -} +} \ No newline at end of file diff --git a/pkg/solana/types.go b/pkg/solana/types.go index 312c6844e..e20f2b600 100644 --- a/pkg/solana/types.go +++ b/pkg/solana/types.go @@ -126,9 +126,10 @@ type Answer struct { // Access controller state type AccessController struct { - Owner solana.PublicKey - Access [32]solana.PublicKey - Len uint64 + Owner solana.PublicKey + ProposedOwner solana.PublicKey + Access [32]solana.PublicKey + Len uint64 } // CL Core OCR2 job spec RelayConfig member for Solana diff --git a/tests/e2e/solclient/deployer.go b/tests/e2e/solclient/deployer.go index a7bb866ce..04d786ef2 100644 --- a/tests/e2e/solclient/deployer.go +++ b/tests/e2e/solclient/deployer.go @@ -25,7 +25,7 @@ const ( // TokenMintAccountSize default size of data required for a new mint account TokenMintAccountSize = uint64(82) TokenAccountSize = uint64(165) - AccessControllerStateAccountSize = uint64(8 + 32 + 8 + 32*64) + AccessControllerStateAccountSize = uint64(8 + 32 + 32 + 8 + 32*64) StoreAccountSize = uint64(8 + 32*4 + 32*128 + 8) OCRTransmissionsAccountSize = uint64(8 + 128 + 8192*24) OCRLeftoverPaymentSize = uint64(32 + 8)