Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 77 additions & 16 deletions sdk/ton/timelock_configurer.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import (
"github.com/xssnick/tonutils-go/address"
"github.com/xssnick/tonutils-go/tlb"
"github.com/xssnick/tonutils-go/ton/wallet"
"github.com/xssnick/tonutils-go/tvm/cell"

"github.com/smartcontractkit/chainlink-ton/cciplib/ton/tlbe"
"github.com/smartcontractkit/chainlink-ton/cciplib/ton/tvm"
"github.com/smartcontractkit/chainlink-ton/pkg/bindings"
"github.com/smartcontractkit/chainlink-ton/pkg/bindings/lib/access/rbac"
"github.com/smartcontractkit/chainlink-ton/pkg/bindings/mcms/timelock"

"github.com/smartcontractkit/mcms/sdk"
Expand Down Expand Up @@ -42,6 +43,26 @@ func NewTimelockConfigurer(w *wallet.Wallet, amount tlb.Coins, opts ...TimelockC

type TimelockConfigurerOption func(*TimelockConfigurer)

// resolveQueryID returns a deterministic QueryID for prepared (skipSend) transactions,
// or a random QueryID for direct-send transactions.
func (c *TimelockConfigurer) resolveQueryID(dst *address.Address, operation string, msg any) (uint64, error) {
if c.skipSend {
body, err := tlb.ToCell(msg)
if err != nil {
return 0, fmt.Errorf("failed to encode %s body for query ID: %w", operation, err)
}

return deterministicPreparedQueryID(dst, operation, body), nil
}

qID, err := tvm.RandomQueryID()
if err != nil {
return 0, fmt.Errorf("failed to generate random query ID: %w", err)
}

return qID, nil
}

func WithDoNotSendTimelockInstructionsOnChain() TimelockConfigurerOption {
return func(c *TimelockConfigurer) {
c.skipSend = true
Expand All @@ -65,22 +86,13 @@ func (c *TimelockConfigurer) UpdateDelay(
msg := timelock.UpdateDelay{
NewDelay: uint32(newDelay),
}
var body *cell.Cell
if c.skipSend {
body, err = tlb.ToCell(msg)
if err != nil {
return types.TransactionResult{}, fmt.Errorf("failed to encode UpdateDelay body: %w", err)
}

msg.QueryID = deterministicPreparedQueryID(dstAddr, "RBACTimelock:UpdateDelay", body)
} else {
msg.QueryID, err = tvm.RandomQueryID()
if err != nil {
return types.TransactionResult{}, fmt.Errorf("failed to generate random query ID: %w", err)
}
msg.QueryID, err = c.resolveQueryID(dstAddr, "RBACTimelock:UpdateDelay", msg)
if err != nil {
return types.TransactionResult{}, err
}

body, err = tlb.ToCell(msg)
body, err := tlb.ToCell(msg)
if err != nil {
return types.TransactionResult{}, fmt.Errorf("failed to encode UpdateDelay body: %w", err)
}
Expand All @@ -106,12 +118,61 @@ func (c *TimelockConfigurer) UpdateDelay(
})
}

// GrantRole grants a timelock role to an address.
// GrantRole sends the RBACTimelock GrantRole message to the given timelock
// address, granting role to targetAddress.
func (c *TimelockConfigurer) GrantRole(
ctx context.Context,
timelockAddress string,
role sdk.TimelockRole,
targetAddress string,
) (types.TransactionResult, error) {
panic("not implemented")
dstAddr, err := address.ParseAddr(timelockAddress)
if err != nil {
return types.TransactionResult{}, fmt.Errorf("invalid timelock address: %w", err)
}

account, err := address.ParseAddr(targetAddress)
if err != nil {
return types.TransactionResult{}, fmt.Errorf("invalid target address: %w", err)
}

roleHash, err := TimelockRoleHash(role)
if err != nil {
return types.TransactionResult{}, err
}

msg := rbac.GrantRole{
Role: tlbe.NewUint256(roleHash),
Account: account,
}

msg.QueryID, err = c.resolveQueryID(dstAddr, "RBACTimelock:GrantRole", msg)
if err != nil {
return types.TransactionResult{}, err
}

body, err := tlb.ToCell(msg)
if err != nil {
return types.TransactionResult{}, fmt.Errorf("failed to encode GrantRole body: %w", err)
}

if c.skipSend {
tx, err := NewTransaction(dstAddr, body.ToBuilder().ToSlice(), c.amount.Nano(), bindings.ShortTimelock, nil, bindings.TypeTimelock, []string{bindings.ShortTimelock, "GrantRole"})
if err != nil {
return types.TransactionResult{}, fmt.Errorf("error encoding transaction: %w", err)
}

return types.TransactionResult{
Hash: "",
ChainFamily: chainsel.FamilyTon,
RawData: tx,
}, nil
}

return SendTx(ctx, TxOpts{
Wallet: c.wallet,
DstAddr: dstAddr,
Amount: c.amount,
Body: body,
})
}
162 changes: 162 additions & 0 deletions sdk/ton/timelock_configurer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,18 @@
"testing"

"github.com/smartcontractkit/chainlink-ton/cciplib/ton/tvm"
"github.com/smartcontractkit/chainlink-ton/pkg/bindings/lib/access/rbac"
"github.com/smartcontractkit/chainlink-ton/pkg/bindings/mcms/timelock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/xssnick/tonutils-go/address"
"github.com/xssnick/tonutils-go/tlb"
"github.com/xssnick/tonutils-go/ton"
"github.com/xssnick/tonutils-go/tvm/cell"

"github.com/smartcontractkit/mcms/internal/testutils/chaintest"
"github.com/smartcontractkit/mcms/sdk"
mcmston "github.com/smartcontractkit/mcms/sdk/ton"
ton_mocks "github.com/smartcontractkit/mcms/sdk/ton/mocks"
"github.com/smartcontractkit/mcms/types"
Expand All @@ -24,7 +27,7 @@
func TestTimelockConfigurer_UpdateDelay(t *testing.T) {
t.Parallel()

const validTimelockAddr = "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8"

Check warning on line 30 in sdk/ton/timelock_configurer_test.go

View check run for this annotation

CL-sonarqube-production / SonarQube Code Analysis

Define a constant instead of duplicating this literal "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8" 3 times.

[S1192] String literals should not be duplicated See more on https://sonarqube.main.prod.cldev.sh/project/issues?id=smartcontractkit_mcms&pullRequest=821&issues=47cd43e5-4481-42e9-9e76-50d730c52fc6&open=47cd43e5-4481-42e9-9e76-50d730c52fc6

tests := []struct {
name string
Expand Down Expand Up @@ -67,8 +70,8 @@
wantPrepared: true,
},
{
name: "invalid timelock address",

Check warning on line 73 in sdk/ton/timelock_configurer_test.go

View check run for this annotation

CL-sonarqube-production / SonarQube Code Analysis

Define a constant instead of duplicating this literal "invalid timelock address" 4 times.

[S1192] String literals should not be duplicated See more on https://sonarqube.main.prod.cldev.sh/project/issues?id=smartcontractkit_mcms&pullRequest=821&issues=0edcc5fe-1ebb-4c76-a09c-8d33d11cbf81&open=0edcc5fe-1ebb-4c76-a09c-8d33d11cbf81
timelockAddress: "not-a-valid-ton-address",

Check warning on line 74 in sdk/ton/timelock_configurer_test.go

View check run for this annotation

CL-sonarqube-production / SonarQube Code Analysis

Define a constant instead of duplicating this literal "not-a-valid-ton-address" 3 times.

[S1192] String literals should not be duplicated See more on https://sonarqube.main.prod.cldev.sh/project/issues?id=smartcontractkit_mcms&pullRequest=821&issues=399f41ec-85d3-454e-b55b-b1a2c87310cc&open=399f41ec-85d3-454e-b55b-b1a2c87310cc
newDelay: 3600,
mockSetup: func(m *ton_mocks.TonAPI) {},
wantErr: "invalid timelock address",
Expand Down Expand Up @@ -148,3 +151,162 @@
})
}
}

func TestTimelockConfigurer_GrantRole(t *testing.T) {

Check warning on line 155 in sdk/ton/timelock_configurer_test.go

View check run for this annotation

CL-sonarqube-production / SonarQube Code Analysis

Rename function "TestTimelockConfigurer_GrantRole" to match the regular expression ^(_|[a-zA-Z0-9]+)$

[S100] Function names should comply with a naming convention See more on https://sonarqube.main.prod.cldev.sh/project/issues?id=smartcontractkit_mcms&pullRequest=821&issues=20efcbc1-a460-4040-b1e9-54c0ef337203&open=20efcbc1-a460-4040-b1e9-54c0ef337203
t.Parallel()

const validTimelockAddr = "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8"
validTargetAddr := address.MustParseAddr("EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8")

tests := []struct {
name string
timelockAddress string
role sdk.TimelockRole
targetAddress string
options []mcmston.TimelockConfigurerOption
mockSetup func(m *ton_mocks.TonAPI)
wantHash string
wantErr string
wantPrepared bool
}{
{
name: "success",
timelockAddress: validTimelockAddr,
role: sdk.TimelockRoleProposer,
targetAddress: validTargetAddr.String(),
mockSetup: func(m *ton_mocks.TonAPI) {
m.EXPECT().CurrentMasterchainInfo(mock.Anything).
Return(&ton.BlockIDExt{}, nil)

apiw := ton_mocks.NewAPIClientWrapped(t)
apiw.EXPECT().GetAccount(mock.Anything, mock.Anything, mock.Anything).
Return(&tlb.Account{}, nil)
apiw.EXPECT().RunGetMethod(mock.Anything, mock.Anything, mock.Anything, mock.Anything).
Return(ton.NewExecutionResult([]any{big.NewInt(5)}), nil)

m.EXPECT().WaitForBlock(mock.Anything).Return(apiw)
m.EXPECT().SendExternalMessageWaitTransaction(mock.Anything, mock.Anything).
Return(&tlb.Transaction{Hash: []byte{0xde, 0xad, 0xbe, 0xef}}, &ton.BlockIDExt{}, []byte{}, nil)
},
wantHash: "deadbeef",
},
{
name: "success - WithDoNotSendTimelockInstructionsOnChain option",
timelockAddress: validTimelockAddr,
role: sdk.TimelockRoleProposer,
targetAddress: validTargetAddr.String(),
options: []mcmston.TimelockConfigurerOption{
mcmston.WithDoNotSendTimelockInstructionsOnChain(),
},
mockSetup: func(m *ton_mocks.TonAPI) {},
wantPrepared: true,
},
{
name: "success - admin role",
timelockAddress: validTimelockAddr,
role: sdk.TimelockRoleAdmin,
targetAddress: validTargetAddr.String(),
options: []mcmston.TimelockConfigurerOption{
mcmston.WithDoNotSendTimelockInstructionsOnChain(),
},
mockSetup: func(m *ton_mocks.TonAPI) {},
wantPrepared: true,
},
{
name: "invalid timelock address",
timelockAddress: "not-a-valid-ton-address",
role: sdk.TimelockRoleProposer,
targetAddress: validTargetAddr.String(),
mockSetup: func(m *ton_mocks.TonAPI) {},
wantErr: "invalid timelock address",
},
{
name: "invalid target address",
timelockAddress: validTimelockAddr,
role: sdk.TimelockRoleProposer,
targetAddress: "not-a-valid-ton-address",
mockSetup: func(m *ton_mocks.TonAPI) {},
wantErr: "invalid target address",
},
{
name: "invalid timelock role",
timelockAddress: validTimelockAddr,
role: sdk.TimelockRole(99),
targetAddress: validTargetAddr.String(),
mockSetup: func(m *ton_mocks.TonAPI) {},
wantErr: "invalid timelock role",
},
{
name: "send transaction fails",
timelockAddress: validTimelockAddr,
role: sdk.TimelockRoleProposer,
targetAddress: validTargetAddr.String(),
mockSetup: func(m *ton_mocks.TonAPI) {
m.EXPECT().CurrentMasterchainInfo(mock.Anything).
Return(&ton.BlockIDExt{}, nil)

apiw := ton_mocks.NewAPIClientWrapped(t)
apiw.EXPECT().GetAccount(mock.Anything, mock.Anything, mock.Anything).
Return(&tlb.Account{}, nil)
apiw.EXPECT().RunGetMethod(mock.Anything, mock.Anything, mock.Anything, mock.Anything).
Return(ton.NewExecutionResult([]any{big.NewInt(5)}), nil)

m.EXPECT().WaitForBlock(mock.Anything).Return(apiw)
m.EXPECT().SendExternalMessageWaitTransaction(mock.Anything, mock.Anything).
Return(nil, nil, nil, errors.New("boom"))
},
wantErr: "failed to send transaction",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

api := ton_mocks.NewTonAPI(t)
chainID := chaintest.Chain7TONID
walletOperator := must(tvm.NewRandomV5R1TestWallet(api, chainID))

tt.mockSetup(api)

configurer := mcmston.NewTimelockConfigurer(walletOperator, tlb.MustFromTON("0.1"), tt.options...)
result, err := configurer.GrantRole(t.Context(), tt.timelockAddress, tt.role, tt.targetAddress)

if tt.wantErr != "" {
require.Error(t, err)
require.ErrorContains(t, err, tt.wantErr)
assert.Empty(t, result.Hash)

return
}

require.NoError(t, err)
assert.Equal(t, tt.wantHash, result.Hash)
if tt.wantPrepared {
tx, ok := result.RawData.(types.Transaction)
require.True(t, ok)
assert.Equal(t, "RBACTimelock", tx.ContractType)
assert.Equal(t, []string{"RBACTimelock", "GrantRole"}, tx.Tags)
body := must(cell.FromBOC(tx.Data))
var msg rbac.GrantRole
require.NoError(t, tlb.LoadFromCell(&msg, body.BeginParse()))

roleHash, err := mcmston.TimelockRoleHash(tt.role)
require.NoError(t, err)
assert.Equal(t, roleHash, msg.Role.Value())
assert.Equal(t, address.MustParseAddr(tt.targetAddress), msg.Account)
assert.NotZero(t, msg.QueryID)

result2, err := configurer.GrantRole(t.Context(), tt.timelockAddress, tt.role, tt.targetAddress)
require.NoError(t, err)
tx2, ok := result2.RawData.(types.Transaction)
require.True(t, ok)
body2 := must(cell.FromBOC(tx2.Data))
var msg2 rbac.GrantRole
require.NoError(t, tlb.LoadFromCell(&msg2, body2.BeginParse()))
assert.Equal(t, tx.Data, tx2.Data)
assert.Equal(t, msg.QueryID, msg2.QueryID)
}
})
}
}
28 changes: 28 additions & 0 deletions sdk/ton/timelock_role.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package ton

import (
"fmt"
"math/big"

"github.com/smartcontractkit/chainlink-ton/pkg/bindings/mcms/timelock"

"github.com/smartcontractkit/mcms/sdk"
)

var timelockRoleHashes = map[sdk.TimelockRole]*big.Int{
sdk.TimelockRoleAdmin: timelock.RoleAdmin,
sdk.TimelockRoleBypasser: timelock.RoleBypasser,
sdk.TimelockRoleCanceller: timelock.RoleCanceller,
sdk.TimelockRoleExecutor: timelock.RoleExecutor,
sdk.TimelockRoleProposer: timelock.RoleProposer,
}

// TimelockRoleHash returns the RBACTimelock AccessControl role hash for role.
func TimelockRoleHash(role sdk.TimelockRole) (*big.Int, error) {
hash, ok := timelockRoleHashes[role]
if !ok {
return nil, fmt.Errorf("invalid timelock role: %d", role)
}

return hash, nil
}
Loading