Skip to content

Commit f2a82ed

Browse files
Malleable Sapient Lib (#347)
* Add TrailsUtils ABI * Malleable Sapient lib * Additional checks * Tuple encoding * Fix overflow protections * More tuple encoding * More edge size validations * scope ABI context in malleable paths (#349) * Update Trails artifacts --------- Co-authored-by: Agusx1211 <aaguilar@polygon.technology>
1 parent a5b038d commit f2a82ed

19 files changed

Lines changed: 4269 additions & 0 deletions

contracts/artifacts/trails-contracts/TrailsUtils.sol/TrailsUtils.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

contracts/contracts.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
seqsale721v0 "github.com/0xsequence/go-sequence/contracts/gen/seq_sale/erc721v0"
1919
"github.com/0xsequence/go-sequence/contracts/gen/supply"
2020
"github.com/0xsequence/go-sequence/contracts/gen/tokens"
21+
trailsutils "github.com/0xsequence/go-sequence/contracts/gen/trailsutils"
2122
v1Factory "github.com/0xsequence/go-sequence/contracts/gen/v1/walletfactory"
2223
v1Estimator "github.com/0xsequence/go-sequence/contracts/gen/v1/walletgasestimator"
2324
v1Guest "github.com/0xsequence/go-sequence/contracts/gen/v1/walletguest"
@@ -43,6 +44,7 @@ var GasEstimator,
4344
IERC1271,
4445
ISapient,
4546
ISapientCompact,
47+
TrailsUtils,
4648
ERC20Mock,
4749
IERC20,
4850
IERC721,
@@ -131,6 +133,7 @@ func init() {
131133
IERC1271 = artifact("IERC1271", ierc1271.IERC1271ABI, "")
132134
ISapient = artifact("ISapient", isapient.ISapientABI, "")
133135
ISapientCompact = artifact("ISapientCompact", isapient.ISapientCompactABI, "")
136+
TrailsUtils = artifact("TRAILS_UTILS", trailsutils.TrailsUtilsABI, trailsutils.TrailsUtilsBin)
134137

135138
IERC20 = artifact("IERC20", tokens.IERC20ABI, "")
136139
IERC721 = artifact("IERC721", tokens.IERC721ABI, "")

contracts/gen/gen.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@
7878
//go:generate go run github.com/0xsequence/ethkit/cmd/ethkit abigen --pkg=isapient --type=ISapient --outFile=./isapient/isapient.gen.go --artifactsFile=../artifacts/wallet-contracts-v3/ISapient.sol/ISapient.json
7979
//go:generate go run github.com/0xsequence/ethkit/cmd/ethkit abigen --pkg=isapient --type=ISapientCompact --outFile=./isapient/isapientcompact.gen.go --artifactsFile=../artifacts/wallet-contracts-v3/ISapient.sol/ISapientCompact.json
8080

81+
//
82+
// trails
83+
//
84+
//go:generate go run github.com/0xsequence/ethkit/cmd/ethkit abigen --pkg=trailsutils --type=TrailsUtils --outFile=./trailsutils/trails_utils.gen.go --artifactsFile=../artifacts/trails-contracts/TrailsUtils.sol/TrailsUtils.json
85+
8186
//
8287
// sequence marketplace
8388
//

contracts/gen/trailsutils/trails_utils.gen.go

Lines changed: 1784 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/sapient/malleable/README.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Malleable Sapient (Go)
2+
3+
Build MalleableSapient signatures by locating byte ranges in call data, and optionally compute the image hash.
4+
5+
## Usage
6+
7+
```go
8+
payload := v3.NewCallsPayload(...)
9+
10+
permitValue := malleable.NewPath().
11+
CallData(0).
12+
ABI(trailsABI, "hydrateExecute").
13+
ArgBytesData("packedPayload").
14+
EncodedCallsPayload().
15+
EncodedCallData(0).
16+
ABI(erc2612ABI, "permit").
17+
ArgSlot("value").
18+
AsSelector()
19+
20+
transferValue := malleable.NewPath().
21+
CallData(0).
22+
ABI(trailsABI, "hydrateExecute").
23+
ArgBytesData("packedPayload").
24+
EncodedCallsPayload().
25+
EncodedCallData(1).
26+
ABI(erc20ABI, "transferFrom").
27+
ArgSlot("_value").
28+
AsSelector()
29+
30+
b := malleable.NewBuilder(payload, &malleable.BuilderOptions{
31+
ValidateRepeats: true,
32+
MergeAdjacentStatic: true,
33+
})
34+
35+
b.Repeat(permitValue, transferValue) // repeat constraint
36+
37+
// mark other malleable fields
38+
b.Malleable(malleable.NewPath().
39+
CallData(0).
40+
ABI(trailsABI, "hydrateExecute").
41+
ArgBytesData("packedPayload").
42+
EncodedCallsPayload().
43+
EncodedCallData(0).
44+
ABI(erc2612ABI, "permit").
45+
ArgSlot("deadline").
46+
AsSelector(),
47+
)
48+
49+
sig, _, err := b.Build()
50+
```
51+
52+
If ABI params are unnamed, use index-based selectors:
53+
54+
```go
55+
value := malleable.NewPath().
56+
CallData(0).
57+
ABI(erc20ABI, "transferFrom").
58+
ArgSlotIndex(2).
59+
AsSelector()
60+
```
61+
62+
After any step that narrows the active range or descends into a new byte
63+
frame, ABI context is cleared. This includes steps like `.Slice(...)`,
64+
`.ArgBytesData(...)`, `.ArgBytesDataIndex(...)`, `.ArgBytesEncoded(...)`,
65+
`.EncodedCallsPayload()`, and `.EncodedCallData(...)`.
66+
67+
Call `.ABI(...)` again before using `.ArgSlot(...)`, `.ArgSlotIndex(...)`,
68+
`.ArgBytesData(...)`, `.ArgBytesDataIndex(...)`, or `.ArgBytesEncoded(...)`
69+
against the new frame.
70+
71+
Compute the image hash:
72+
73+
```go
74+
hash, err := malleable.ComputeImageHash(payload, sig, chainID)
75+
```

lib/sapient/malleable/builder.go

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
package malleable
2+
3+
import (
4+
"fmt"
5+
"sort"
6+
7+
"github.com/0xsequence/ethkit/go-ethereum/crypto"
8+
v3 "github.com/0xsequence/go-sequence/core/v3"
9+
)
10+
11+
// maxUint16 defines the maximum allowed value for size validation of sections using uint16 wire format.
12+
const (
13+
maxUint16 = 0xFFFF
14+
)
15+
16+
type BuilderOptions struct {
17+
ValidateRepeats bool
18+
MergeAdjacentStatic bool
19+
MaxOffset uint32
20+
MaxSize uint32
21+
}
22+
23+
type Builder struct {
24+
payload *v3.CallsPayload
25+
options BuilderOptions
26+
malleable []Selector
27+
repeats []repeatSelector
28+
}
29+
30+
type repeatSelector struct {
31+
a Selector
32+
b Selector
33+
}
34+
35+
type Plan struct {
36+
Static []StaticSection
37+
Repeat []RepeatSection
38+
}
39+
40+
func (p *Plan) DebugString() string {
41+
out := "static:"
42+
for _, s := range p.Static {
43+
out += fmt.Sprintf(" [t=%d c=%d s=%d]", s.TIndex, s.CIndex, s.Size)
44+
}
45+
out += " repeat:"
46+
for _, r := range p.Repeat {
47+
out += fmt.Sprintf(" [t=%d c=%d s=%d t2=%d c2=%d]", r.TIndex, r.CIndex, r.Size, r.TIndex2, r.CIndex2)
48+
}
49+
return out
50+
}
51+
52+
func NewBuilder(payload *v3.CallsPayload, opts *BuilderOptions) *Builder {
53+
options := BuilderOptions{
54+
MaxOffset: 0xFFFF,
55+
MaxSize: 0xFFFF,
56+
ValidateRepeats: false,
57+
MergeAdjacentStatic: true,
58+
}
59+
if opts != nil {
60+
if opts.MaxOffset != 0 {
61+
options.MaxOffset = opts.MaxOffset
62+
}
63+
if opts.MaxSize != 0 {
64+
options.MaxSize = opts.MaxSize
65+
}
66+
options.ValidateRepeats = opts.ValidateRepeats
67+
options.MergeAdjacentStatic = opts.MergeAdjacentStatic
68+
}
69+
70+
return &Builder{
71+
payload: payload,
72+
options: options,
73+
}
74+
}
75+
76+
func (b *Builder) Malleable(sel Selector) *Builder {
77+
b.malleable = append(b.malleable, sel)
78+
return b
79+
}
80+
81+
func (b *Builder) Repeat(a Selector, b2 Selector) *Builder {
82+
b.repeats = append(b.repeats, repeatSelector{a: a, b: b2})
83+
return b
84+
}
85+
86+
func (b *Builder) Build() ([]byte, *Plan, error) {
87+
if b.payload == nil {
88+
return nil, nil, fmt.Errorf("payload is nil")
89+
}
90+
if len(b.payload.Calls) > 128 {
91+
return nil, nil, fmt.Errorf("too many calls (%d): tindex is 7-bit", len(b.payload.Calls))
92+
}
93+
94+
exByCall := make([][]Span, len(b.payload.Calls))
95+
96+
addExclude := func(r ByteRange) error {
97+
if r.CallIndex < 0 || r.CallIndex >= len(b.payload.Calls) {
98+
return fmt.Errorf("tindex out of range: %d", r.CallIndex)
99+
}
100+
dataLen := len(b.payload.Calls[r.CallIndex].Data)
101+
if r.Offset < 0 || r.Size < 0 {
102+
return fmt.Errorf("span offset/size negative")
103+
}
104+
if r.Size > dataLen || r.Offset > dataLen-r.Size {
105+
return fmt.Errorf("span out of bounds: offset=%d size=%d > %d", r.Offset, r.Size, dataLen)
106+
}
107+
exByCall[r.CallIndex] = append(exByCall[r.CallIndex], Span{Start: r.Offset, Len: r.Size})
108+
return nil
109+
}
110+
111+
for _, sel := range b.malleable {
112+
ranges, err := sel.Resolve(b.payload)
113+
if err != nil {
114+
return nil, nil, fmt.Errorf("malleable %s: %w", sel.String(), err)
115+
}
116+
for _, r := range ranges {
117+
if err := addExclude(r); err != nil {
118+
return nil, nil, fmt.Errorf("malleable %s: %w", sel.String(), err)
119+
}
120+
}
121+
}
122+
123+
var repeats []RepeatSection
124+
for _, rp := range b.repeats {
125+
rangesA, err := rp.a.Resolve(b.payload)
126+
if err != nil {
127+
return nil, nil, fmt.Errorf("repeat a %s: %w", rp.a.String(), err)
128+
}
129+
rangesB, err := rp.b.Resolve(b.payload)
130+
if err != nil {
131+
return nil, nil, fmt.Errorf("repeat b %s: %w", rp.b.String(), err)
132+
}
133+
if len(rangesA) != 1 || len(rangesB) != 1 {
134+
return nil, nil, fmt.Errorf("repeat expects single range per selector")
135+
}
136+
a := rangesA[0]
137+
bb := rangesB[0]
138+
if a.Size != bb.Size {
139+
return nil, nil, fmt.Errorf("repeat size mismatch: %d vs %d", a.Size, bb.Size)
140+
}
141+
if err := addExclude(a); err != nil {
142+
return nil, nil, fmt.Errorf("repeat a: %w", err)
143+
}
144+
if err := addExclude(bb); err != nil {
145+
return nil, nil, fmt.Errorf("repeat b: %w", err)
146+
}
147+
if b.options.ValidateRepeats {
148+
sectionA, err := a.Slice(b.payload)
149+
if err != nil {
150+
return nil, nil, err
151+
}
152+
sectionB, err := bb.Slice(b.payload)
153+
if err != nil {
154+
return nil, nil, err
155+
}
156+
if crypto.Keccak256Hash(sectionA) != crypto.Keccak256Hash(sectionB) {
157+
return nil, nil, fmt.Errorf("repeat section mismatch")
158+
}
159+
}
160+
if a.Offset > maxUint16 || bb.Offset > maxUint16 {
161+
return nil, nil, fmt.Errorf("repeat offset exceeds uint16 range (max %d): a.Offset=%d b.Offset=%d", maxUint16, a.Offset, bb.Offset)
162+
}
163+
if a.Size > maxUint16 {
164+
return nil, nil, fmt.Errorf("repeat size exceeds uint16 range (max %d): a.Size=%d", maxUint16, a.Size)
165+
}
166+
repeats = append(repeats, RepeatSection{
167+
TIndex: uint8(a.CallIndex),
168+
CIndex: uint16(a.Offset),
169+
Size: uint16(a.Size),
170+
TIndex2: uint8(bb.CallIndex),
171+
CIndex2: uint16(bb.Offset),
172+
})
173+
}
174+
175+
var statics []SpanWithCall
176+
for t := range b.payload.Calls {
177+
length := len(b.payload.Calls[t].Data)
178+
ex := mergeSpans(exByCall[t])
179+
cursor := 0
180+
for _, s := range ex {
181+
if cursor < s.Start {
182+
statics = append(statics, SpanWithCall{CallIndex: t, Span: Span{Start: cursor, Len: s.Start - cursor}})
183+
}
184+
cursor = max(cursor, s.Start+s.Len)
185+
}
186+
if cursor < length {
187+
statics = append(statics, SpanWithCall{CallIndex: t, Span: Span{Start: cursor, Len: length - cursor}})
188+
}
189+
}
190+
191+
sort.Slice(statics, func(i, j int) bool {
192+
if statics[i].CallIndex != statics[j].CallIndex {
193+
return statics[i].CallIndex < statics[j].CallIndex
194+
}
195+
return statics[i].Start < statics[j].Start
196+
})
197+
198+
if b.options.MergeAdjacentStatic {
199+
statics = mergeAdjacentStatics(statics)
200+
}
201+
202+
staticSections, err := b.encodeStaticSections(statics)
203+
if err != nil {
204+
return nil, nil, err
205+
}
206+
207+
signature, err := EncodeSignature(staticSections, repeats)
208+
if err != nil {
209+
return nil, nil, err
210+
}
211+
212+
return signature, &Plan{Static: staticSections, Repeat: repeats}, nil
213+
}
214+
215+
type SpanWithCall struct {
216+
CallIndex int
217+
Span
218+
}
219+
220+
func (b *Builder) encodeStaticSections(statics []SpanWithCall) ([]StaticSection, error) {
221+
var sections []StaticSection
222+
for _, s := range statics {
223+
if s.Len == 0 {
224+
continue
225+
}
226+
if s.CallIndex < 0 || s.CallIndex >= len(b.payload.Calls) {
227+
return nil, fmt.Errorf("tindex out of range: %d", s.CallIndex)
228+
}
229+
offset := s.Start
230+
length := s.Len
231+
for length > 0 {
232+
chunk := length
233+
if uint32(chunk) > b.options.MaxSize {
234+
chunk = int(b.options.MaxSize)
235+
}
236+
if uint32(offset) > b.options.MaxOffset {
237+
return nil, fmt.Errorf("cindex too large: %d", offset)
238+
}
239+
if offset > maxUint16 || chunk > maxUint16 {
240+
return nil, fmt.Errorf("static section offset/size exceeds uint16 range (max %d): offset=%d chunk=%d", maxUint16, offset, chunk)
241+
}
242+
sections = append(sections, StaticSection{
243+
TIndex: uint8(s.CallIndex),
244+
CIndex: uint16(offset),
245+
Size: uint16(chunk),
246+
})
247+
offset += chunk
248+
length -= chunk
249+
}
250+
}
251+
return sections, nil
252+
}
253+
254+
func mergeSpans(spans []Span) []Span {
255+
if len(spans) == 0 {
256+
return nil
257+
}
258+
sort.Slice(spans, func(i, j int) bool { return spans[i].Start < spans[j].Start })
259+
out := []Span{spans[0]}
260+
for _, s := range spans[1:] {
261+
last := &out[len(out)-1]
262+
if s.Start <= last.Start+last.Len {
263+
end := max(last.Start+last.Len, s.Start+s.Len)
264+
last.Len = end - last.Start
265+
} else {
266+
out = append(out, s)
267+
}
268+
}
269+
return out
270+
}
271+
272+
func mergeAdjacentStatics(statics []SpanWithCall) []SpanWithCall {
273+
if len(statics) == 0 {
274+
return statics
275+
}
276+
out := []SpanWithCall{statics[0]}
277+
for _, s := range statics[1:] {
278+
last := &out[len(out)-1]
279+
if s.CallIndex == last.CallIndex && s.Start == last.Start+last.Len {
280+
last.Len += s.Len
281+
} else {
282+
out = append(out, s)
283+
}
284+
}
285+
return out
286+
}

0 commit comments

Comments
 (0)