Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
27bb97b
Restructure TP flow with onlyRouter access
krebernisak Aug 25, 2026
6068b30
Use Cell.EMPTY
krebernisak Aug 25, 2026
1897f1a
Fix TP test - use bindings
krebernisak Aug 25, 2026
0299a8a
Add forceAbiExport to TP contract metadata
krebernisak Aug 25, 2026
76d77ed
Use a set vs. map for allowedDepositNamespaces
krebernisak Aug 25, 2026
e9417d3
burn/mint tp - remove unnecessary wallet addr check on excess
krebernisak Aug 25, 2026
4daf24d
Route inbound ReleaseOrMint through Router as pool chokepoint
krebernisak Sep 14, 2026
f15f152
Tighten chokepoint comments and add relay failure-path coverage
krebernisak Sep 14, 2026
3870497
Sync Go bindings and deployment with Router-only pool ABI
krebernisak Sep 15, 2026
5e44641
Remove resurrected MockTokenPool fixture
krebernisak Sep 15, 2026
40b5e09
bindings: model AllowedDepositNamespaces as tlbe.Dict[uint32, struct{}]
krebernisak Sep 15, 2026
5286996
tlbe: add comparable Uint128; use tlbe.Dict for CursedSubjects
krebernisak Sep 15, 2026
adc61b5
tlbe: make boxed uints comparable; fix pointer-key dict decode
krebernisak Sep 15, 2026
be2eb3e
fix(ccip): align pool admission with EVM override semantics and decod…
krebernisak Sep 16, 2026
69ce559
perf(ccip): lazy-load pool config fields in guards and hot paths
krebernisak Sep 17, 2026
04b6f0c
Merge branch 'main' into feat/tp-only-router (+fix duplicated tests r…
krebernisak Sep 17, 2026
f9f8543
refactor(cciplib): unify tlbe uint tests, drop dead BigUint
krebernisak Sep 17, 2026
6cbc8ba
Fix lint
krebernisak Sep 17, 2026
ac5b89b
fix(cciplib): use require.Equal/NotEqual in AreComparable (testifylin…
krebernisak Sep 17, 2026
5a9fea4
Fix yarn.lock
krebernisak Sep 17, 2026
f0e6715
Merge branch 'main' into feat/tp-only-router
krebernisak Sep 17, 2026
78c2b90
chore(nix): update lock.nix hashes
app-token-issuer-integrations[bot] Sep 17, 2026
18cb546
Merge branch 'main' into feat/tp-only-router
krebernisak Sep 22, 2026
09172bd
chore(nix): update lock.nix hashes
app-token-issuer-integrations[bot] Sep 22, 2026
ac82583
Add ./cciplib local replace (tmp)
krebernisak Sep 22, 2026
6ada85c
chore(nix): update lock.nix hashes
app-token-issuer-integrations[bot] Sep 22, 2026
2c4b575
Merge branch 'main' into feat/tp-only-router
krebernisak Sep 22, 2026
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
352 changes: 175 additions & 177 deletions cciplib/ton/tlbe/biguint.go

Large diffs are not rendered by default.

270 changes: 262 additions & 8 deletions cciplib/ton/tlbe/biguint_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package tlbe // tlb extras

import (
"encoding/json"
"errors"
"fmt"
"math/big"
Expand All @@ -17,10 +18,187 @@ type legacyValues struct {
}

type typedValues struct {
Address Uint160 `tlb:"."`
Root Uint256 `tlb:"."`
}

// ptrTypedValues covers the pointer-field form still used by the shared struct
// definitions (e.g. *tlbe.Uint256), which must keep working unchanged.
type ptrTypedValues struct {
Address *Uint160 `tlb:"."`
Root *Uint256 `tlb:"."`
}

// uint128SetHolder guards the generic path used by CursedSubjects: a struct
// field with a tlb:"." tag holding a *Dict[Uint128, struct{}].
type uint128SetHolder struct {
Subjects *Dict[Uint128, struct{}] `tlb:"."`
}

// bigFromHex builds a big.Int from a hex string (helper for readable test cases).
func bigFromHex(t *testing.T, s string) *big.Int {
t.Helper()
v, ok := new(big.Int).SetString(s, 16)
require.True(t, ok)
return v
}

// TestUintWrappers_WireCompatWithRawStore locks in that the byte-based codec
// produces exactly the same cell as a raw fixed-width store, so on-chain data
// stays bit-identical to the previous BigUint/StoreBigInt path.
func TestUintWrappers_WireCompatWithRawStore(t *testing.T) {
v := new(big.Int).Add(new(big.Int).Lsh(big.NewInt(9), 200), big.NewInt(1234567))

cases := []struct {
name string
boxed any
raw *cell.Cell
}{
{"uint128", *NewUint128(v), cell.BeginCell().MustStoreBigInt(AsUnsigned(v, 128), 128).EndCell()},
{"uint160", *NewUint160(v), cell.BeginCell().MustStoreBigInt(AsUnsigned(v, 160), 160).EndCell()},
{"uint256", *NewUint256(v), cell.BeginCell().MustStoreBigInt(AsUnsigned(v, 256), 256).EndCell()},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
c, err := tlb.ToCell(tc.boxed)
require.NoError(t, err)
require.Equal(t, tc.raw.Hash(), c.Hash(),
"boxed encoding must match the raw fixed-width big-endian store")
})
}
}

// TestUint128_Values checks the in-memory layout: big-endian bytes, so a bit set
// at position N lands in the expected word of F.
func TestUint128_Values(t *testing.T) {
// A value above the 64-bit range exercises multiple bytes.
v := new(big.Int).Add(new(big.Int).Lsh(big.NewInt(1), 100), big.NewInt(12345))

u := NewUint128(v)
require.Equal(t, v, u.ToBigInt())
require.Equal(t, uint8(0x0), u.F[0]) // high bytes zero
require.Equal(t, uint8(0x10), u.F[3]) // 2^100 sets bit 4 of byte 3

// Zero and max round-trip through the bytes.
require.Equal(t, big.NewInt(0), NewUint128(big.NewInt(0)).ToBigInt())

maxVal := new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 128), big.NewInt(1))
require.Equal(t, maxVal, NewUint128(maxVal).ToBigInt())
}

// TestUintWrappers_AreComparable locks in the core property: the boxed fixed-width
// uints are value types with == semantics (unlike a big.Int alias), so they can
// be used as Go map / tlbe.Dict keys.
func TestUintWrappers_AreComparable(t *testing.T) {
v128 := new(big.Int).Lsh(big.NewInt(1), 100)
a128, b128 := *NewUint128(v128), *NewUint128(v128)
require.Equal(t, a128, b128, "equal Uint128 values must compare equal")
require.NotEqual(t, a128, *NewUint128(big.NewInt(1)), "different Uint128 values must compare unequal")

v160 := new(big.Int).Lsh(big.NewInt(1), 159)
a160, b160 := *NewUint160(v160), *NewUint160(v160)
require.Equal(t, a160, b160, "equal Uint160 values must compare equal")
require.NotEqual(t, a160, *NewUint160(big.NewInt(1)), "different Uint160 values must compare unequal")

v256 := new(big.Int).Lsh(big.NewInt(1), 255)
a256, b256 := *NewUint256(v256), *NewUint256(v256)
require.Equal(t, a256, b256, "equal Uint256 values must compare equal")
require.NotEqual(t, a256, *NewUint256(big.NewInt(1)), "different Uint256 values must compare unequal")
}

// TestUintWrappers_DictKeyRoundTrip is the regression test for the pointer-key
// bug: boxed uint keys of every width must survive
// ToCell -> LoadDict -> NewDictFromDictionary (the previous *Uint256 key form
// mis-keyed and panicked in tonutils' reflection path).
func TestUintWrappers_DictKeyRoundTrip(t *testing.T) {
t.Run("uint128", func(t *testing.T) {
d := NewDict[Uint128, struct{}](map[Uint128]struct{}{
*NewUint128(big.NewInt(5)): {},
*NewUint128(new(big.Int).Lsh(big.NewInt(1), 127)): {},
})

c, err := d.ToCell()
require.NoError(t, err)

dict, err := c.BeginParse().LoadDict(128)
require.NoError(t, err)

restored, err := NewDictFromDictionary[Uint128, struct{}](dict)
require.NoError(t, err)
require.Len(t, restored.AsMap(), 2)

_, ok := restored.Get(*NewUint128(big.NewInt(5)))
require.True(t, ok)
_, ok = restored.Get(*NewUint128(new(big.Int).Lsh(big.NewInt(1), 127)))
require.True(t, ok)
})

t.Run("uint160", func(t *testing.T) {
d := NewDict[Uint160, bool](map[Uint160]bool{
*NewUint160(big.NewInt(5)): true,
*NewUint160(new(big.Int).Lsh(big.NewInt(1), 159)): false,
*NewUint160(bigFromHex(t, "ffffffffffffffffffffffffffffffffffffffff")): true,
})

c, err := d.ToCell()
require.NoError(t, err)

dict, err := c.BeginParse().LoadDict(160)
require.NoError(t, err)

restored, err := NewDictFromDictionary[Uint160, bool](dict)
require.NoError(t, err)
require.Len(t, restored.AsMap(), 3)

v, ok := restored.Get(*NewUint160(big.NewInt(5)))
require.True(t, ok)
require.True(t, v)

v, ok = restored.Get(*NewUint160(new(big.Int).Lsh(big.NewInt(1), 159)))
require.True(t, ok)
require.False(t, v)
})

t.Run("uint256", func(t *testing.T) {
d := NewDict[Uint256, uint64](map[Uint256]uint64{
*NewUint256(big.NewInt(7)): 42,
*NewUint256(new(big.Int).Lsh(big.NewInt(1), 255)): 99,
})

c, err := d.ToCell()
require.NoError(t, err)

dict, err := c.BeginParse().LoadDict(256)
require.NoError(t, err)

restored, err := NewDictFromDictionary[Uint256, uint64](dict)
require.NoError(t, err)
require.Len(t, restored.AsMap(), 2)

v, ok := restored.Get(*NewUint256(new(big.Int).Lsh(big.NewInt(1), 255)))
require.True(t, ok)
require.Equal(t, uint64(99), v)
})
}

// TestUint128_DictStructFieldRoundTrip guards the generic dict-in-struct path
// used by CursedSubjects.
func TestUint128_DictStructFieldRoundTrip(t *testing.T) {
original := uint128SetHolder{
Subjects: NewDict[Uint128, struct{}](map[Uint128]struct{}{
*NewUint128(big.NewInt(3)): {},
}),
}

c, err := tlb.ToCell(original)
require.NoError(t, err)

var decoded uint128SetHolder
require.NoError(t, tlb.LoadFromCell(&decoded, c.BeginParse()))
require.Len(t, decoded.Subjects.AsMap(), 1)
}

func TestUintWrappers_MaskSignBit(t *testing.T) {
testCases := []struct {
name string
Expand Down Expand Up @@ -59,12 +237,19 @@ func TestUintWrappers_MaskSignBit(t *testing.T) {
err = tlb.LoadFromCell(&typed, c.BeginParse())
require.NoError(t, err)

require.NotNil(t, typed.Address.Value(), "address value is nil")
require.Equal(t, expectedAddr, typed.Address.Value(), "address mismatch")

require.NotNil(t, typed.Root.Value(), "root value is nil")
require.Equal(t, expectedRoot, typed.Root.Value(), "root mismatch")

// Pointer fields must decode too.
var ptrTyped ptrTypedValues
err = tlb.LoadFromCell(&ptrTyped, c.BeginParse())
require.NoError(t, err)

require.NotNil(t, ptrTyped.Address, "pointer address is nil")
require.Equal(t, expectedAddr, ptrTyped.Address.Value(), "pointer address mismatch")
require.NotNil(t, ptrTyped.Root, "pointer root is nil")
require.Equal(t, expectedRoot, ptrTyped.Root.Value(), "pointer root mismatch")

// Testing legacy big.Int loading - should interpret as signed integers
var legacy legacyValues
err = tlb.LoadFromCell(&legacy, c.BeginParse())
Expand All @@ -81,8 +266,8 @@ func TestUintWrappers_RoundTrip(t *testing.T) {
root := leadingBytes(32, 0x7f)

original := typedValues{
Address: NewUint160(new(big.Int).SetBytes(addr)),
Root: NewUint256(new(big.Int).SetBytes(root)),
Address: *NewUint160(new(big.Int).SetBytes(addr)),
Root: *NewUint256(new(big.Int).SetBytes(root)),
}

cellValue, err := tlb.ToCell(original)
Expand All @@ -92,11 +277,9 @@ func TestUintWrappers_RoundTrip(t *testing.T) {
err = tlb.LoadFromCell(&decoded, cellValue.BeginParse())
require.NoError(t, err)

require.NotNil(t, decoded.Address.Value(), "address value is nil after roundtrip")
require.Equal(t, new(big.Int).SetBytes(addr), decoded.Address.Value(), "address mismatch after roundtrip")

require.NotNil(t, decoded.Root.Value(), "root value is nil after roundtrip")
require.Equal(t, new(big.Int).SetBytes(root), decoded.Root.Value(), "root mismatch after roundtrip")
require.Equal(t, original, decoded, "value types must be comparable with ==")

var legacy legacyValues
err = tlb.LoadFromCell(&legacy, cellValue.BeginParse())
Expand Down Expand Up @@ -133,3 +316,74 @@ func encodeFixed(addr, root []byte) (*cell.Cell, error) {

return builder.EndCell(), nil
}

// TestUintWrappers_CellRoundTripAndWidth checks each type's cell width and value.
func TestUintWrappers_CellRoundTripAndWidth(t *testing.T) {
testCases := []struct {
name string
build func(*big.Int) (any, uint)
}{
{"uint128", func(v *big.Int) (any, uint) { return *NewUint128(v), 128 }},
{"uint160", func(v *big.Int) (any, uint) { return *NewUint160(v), 160 }},
{"uint256", func(v *big.Int) (any, uint) { return *NewUint256(v), 256 }},
}

value := new(big.Int).Add(new(big.Int).Lsh(big.NewInt(3), 120), big.NewInt(7))

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
original, bits := tc.build(value)

c, err := tlb.ToCell(original)
require.NoError(t, err)
require.Equal(t, bits, c.BitsSize())

switch x := original.(type) {
case Uint128:
var decoded Uint128
require.NoError(t, decoded.LoadFromCell(c.BeginParse()))
require.Equal(t, x, decoded)
require.Equal(t, x.ToBigInt(), decoded.ToBigInt())
case Uint160:
var decoded Uint160
require.NoError(t, decoded.LoadFromCell(c.BeginParse()))
require.Equal(t, x, decoded)
case Uint256:
var decoded Uint256
require.NoError(t, decoded.LoadFromCell(c.BeginParse()))
require.Equal(t, x, decoded)
}
})
}
}

// TestUintWrappers_JSONHexRoundTrip checks canonical hex JSON for all three types.
func TestUintWrappers_JSONHexRoundTrip(t *testing.T) {
for _, jsonCase := range []struct {
value any
want string
}{
{*NewUint128(big.NewInt(255)), `"0xff"`},
{*NewUint160(big.NewInt(255)), `"0xff"`},
{*NewUint256(big.NewInt(255)), `"0xff"`},
} {
payload, err := json.Marshal(jsonCase.value)
require.NoError(t, err)
require.JSONEq(t, jsonCase.want, string(payload))

switch x := jsonCase.value.(type) {
case Uint128:
var d Uint128
require.NoError(t, json.Unmarshal(payload, &d))
require.Equal(t, x, d)
case Uint160:
var d Uint160
require.NoError(t, json.Unmarshal(payload, &d))
require.Equal(t, x, d)
case Uint256:
var d Uint256
require.NoError(t, json.Unmarshal(payload, &d))
require.Equal(t, x, d)
}
}
}
2 changes: 1 addition & 1 deletion cmd/chainlink-ton-extras/lock.nix
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Notice: `pkgs.lib.fakeHash` can be used as a placeholder,
# but `lock-nix-tidy` will only replace actual hashes.
{pkgs}: {
chainlink-ton-extras = "sha256-lHYNeIUSxyJyHgoJcd/VjwLOynH2AP5Fp8nK4YpKhhU=";
chainlink-ton-extras = "sha256-CvAHD2AeJq51erMWhXj+u2FHFBIyREuGX7LjGbQagXk=";
}
2 changes: 1 addition & 1 deletion cmd/chainlink-ton/lock.nix
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Notice: `pkgs.lib.fakeHash` can be used as a placeholder,
# but `lock-nix-tidy` will only replace actual hashes.
{pkgs}: {
chainlink-ton = "sha256-lHYNeIUSxyJyHgoJcd/VjwLOynH2AP5Fp8nK4YpKhhU=";
chainlink-ton = "sha256-CvAHD2AeJq51erMWhXj+u2FHFBIyREuGX7LjGbQagXk=";
}
34 changes: 34 additions & 0 deletions contracts/contracts/ccip/ccipsend_executor/contract.tolk
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,40 @@ fun onInternalMessage(in: InMessage) {
assert(this.state.load().tokenPool == in.senderAddress, CCIPSendExecutor_Error.Unauthorized);
this.onConfirmWithdraw(msg);
}
// Structured pool failures can arrive either before custody withdrawal
// or while the withdrawal/finalization phase is in progress.
TokenPool_LockOrBurnFailure => {
val st = lazy CCIPSendExecutor_Data.fromCell(contract.getData());
if (st.state is Cell<CCIPSendExecutor_State_TokenPool_LockOrBurn>) {
var this = CCIPSendExecutor<CCIPSendExecutor_State_TokenPool_LockOrBurn>.load();
assert(this.state.load().tokenPool == in.senderAddress, CCIPSendExecutor_Error.Unauthorized);
this.exitWithError(msg.errorCode as uint256);
} else {
var this = CCIPSendExecutor<CCIPSendExecutor_State_TokenPool_Withdraw>.load();
assert(this.state.load().tokenPool == in.senderAddress, CCIPSendExecutor_Error.Unauthorized);
this.exitWithError(msg.errorCode as uint256);
}
}
// Router policy rejection, a pre-admission pool bounce, or a bounced
// Router-wallet withdrawal is reported by the configured Router.
Router_TokenPoolLockOrBurnFailed => {
val st = lazy CCIPSendExecutor_Data.fromCell(contract.getData());
if (st.state is Cell<CCIPSendExecutor_State_TokenPool_LockOrBurn>) {
var this = CCIPSendExecutor<CCIPSendExecutor_State_TokenPool_LockOrBurn>.load();
val state = lazy this.state.load();
val addresses = lazy this.addresses.load();
assert(addresses.router == in.senderAddress, CCIPSendExecutor_Error.Unauthorized);
assert(state.tokenPool == msg.tokenPool, CCIPSendExecutor_Error.Unauthorized);
this.exitWithError(CCIPSendExecutor_Error.TokenPoolBounce as uint256);
} else {
var this = CCIPSendExecutor<CCIPSendExecutor_State_TokenPool_Withdraw>.load();
val state = lazy this.state.load();
val addresses = lazy this.addresses.load();
assert(addresses.router == in.senderAddress, CCIPSendExecutor_Error.Unauthorized);
assert(state.tokenPool == msg.tokenPool, CCIPSendExecutor_Error.Unauthorized);
this.exitWithError(CCIPSendExecutor_Error.TokenPoolBounce as uint256);
}
}
else => {
assert (in.body.isEmpty()) throw 0xFFFF;
},
Expand Down
1 change: 1 addition & 0 deletions contracts/contracts/ccip/ccipsend_executor/errors.tolk
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ enum CCIPSendExecutor_Error {
InsufficientFee
FeeQuoterBounce
TokenNotEnabled
TokenPoolBounce
}
5 changes: 4 additions & 1 deletion contracts/contracts/ccip/ccipsend_executor/messages.tolk
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import "../../lib/jetton/messages"

import "../router/messages"
import "../onramp/messages"
import "../fee_quoter/messages"
import "../token_admin_registry_entry/messages"
Expand All @@ -14,7 +15,9 @@ type CCIPSendExecutor_InMessage =
| TokenAdminRegistryEntry_ReturnTokenInfo
| ResponseWalletAddress
| TokenPool_LockOrBurnWithdraw
| TokenPool_LockOrBurnFinished;
| TokenPool_LockOrBurnFinished
| TokenPool_LockOrBurnFailure
| Router_TokenPoolLockOrBurnFailed;

type FeeQuoter_MessageValidated_Any = FeeQuoter_MessageValidated<RemainingBitsAndRefs>
type FeeQuoter_MessageValidationFailed_Any = FeeQuoter_MessageValidationFailed<RemainingBitsAndRefs>
Expand Down
Loading
Loading