-
Notifications
You must be signed in to change notification settings - Fork 12
Malleable Sapient Lib #347
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
f0c1568
Add TrailsUtils ABI
ScreamingHawk 0b3eff0
Malleable Sapient lib
ScreamingHawk 3a81806
Additional checks
ScreamingHawk 2b118cc
Tuple encoding
ScreamingHawk eabe739
Fix overflow protections
ScreamingHawk e2ca4b8
More tuple encoding
ScreamingHawk 4280ea4
More edge size validations
ScreamingHawk 8cdf29c
scope ABI context in malleable paths (#349)
Agusx1211 3d278cf
Update Trails artifacts
ScreamingHawk File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
1 change: 1 addition & 0 deletions
1
contracts/artifacts/trails-contracts/TrailsUtils.sol/TrailsUtils.json
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or 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
This file contains hidden or 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
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or 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,75 @@ | ||
| # Malleable Sapient (Go) | ||
|
|
||
| Build MalleableSapient signatures by locating byte ranges in call data, and optionally compute the image hash. | ||
|
|
||
| ## Usage | ||
|
|
||
| ```go | ||
| payload := v3.NewCallsPayload(...) | ||
|
|
||
| permitValue := malleable.NewPath(). | ||
| CallData(0). | ||
| ABI(trailsABI, "hydrateExecute"). | ||
| ArgBytesData("packedPayload"). | ||
| EncodedCallsPayload(). | ||
| EncodedCallData(0). | ||
| ABI(erc2612ABI, "permit"). | ||
| ArgSlot("value"). | ||
| AsSelector() | ||
|
|
||
| transferValue := malleable.NewPath(). | ||
| CallData(0). | ||
| ABI(trailsABI, "hydrateExecute"). | ||
| ArgBytesData("packedPayload"). | ||
| EncodedCallsPayload(). | ||
| EncodedCallData(1). | ||
| ABI(erc20ABI, "transferFrom"). | ||
| ArgSlot("_value"). | ||
| AsSelector() | ||
|
|
||
| b := malleable.NewBuilder(payload, &malleable.BuilderOptions{ | ||
| ValidateRepeats: true, | ||
| MergeAdjacentStatic: true, | ||
| }) | ||
|
|
||
| b.Repeat(permitValue, transferValue) // repeat constraint | ||
|
|
||
| // mark other malleable fields | ||
| b.Malleable(malleable.NewPath(). | ||
| CallData(0). | ||
| ABI(trailsABI, "hydrateExecute"). | ||
| ArgBytesData("packedPayload"). | ||
| EncodedCallsPayload(). | ||
| EncodedCallData(0). | ||
| ABI(erc2612ABI, "permit"). | ||
| ArgSlot("deadline"). | ||
| AsSelector(), | ||
| ) | ||
|
|
||
| sig, _, err := b.Build() | ||
| ``` | ||
|
|
||
| If ABI params are unnamed, use index-based selectors: | ||
|
|
||
| ```go | ||
| value := malleable.NewPath(). | ||
| CallData(0). | ||
| ABI(erc20ABI, "transferFrom"). | ||
| ArgSlotIndex(2). | ||
| AsSelector() | ||
| ``` | ||
|
|
||
| After any step that narrows the active range or descends into a new byte | ||
| frame, ABI context is cleared. This includes steps like `.Slice(...)`, | ||
| `.ArgBytesData(...)`, `.ArgBytesDataIndex(...)`, `.ArgBytesEncoded(...)`, | ||
| `.EncodedCallsPayload()`, and `.EncodedCallData(...)`. | ||
|
|
||
| Call `.ABI(...)` again before using `.ArgSlot(...)`, `.ArgSlotIndex(...)`, | ||
| `.ArgBytesData(...)`, `.ArgBytesDataIndex(...)`, or `.ArgBytesEncoded(...)` | ||
| against the new frame. | ||
|
|
||
| Compute the image hash: | ||
|
|
||
| ```go | ||
| hash, err := malleable.ComputeImageHash(payload, sig, chainID) | ||
| ``` |
This file contains hidden or 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,286 @@ | ||
| package malleable | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "sort" | ||
|
|
||
| "github.com/0xsequence/ethkit/go-ethereum/crypto" | ||
| v3 "github.com/0xsequence/go-sequence/core/v3" | ||
| ) | ||
|
|
||
| // maxUint16 defines the maximum allowed value for size validation of sections using uint16 wire format. | ||
| const ( | ||
| maxUint16 = 0xFFFF | ||
| ) | ||
|
|
||
| type BuilderOptions struct { | ||
| ValidateRepeats bool | ||
| MergeAdjacentStatic bool | ||
| MaxOffset uint32 | ||
| MaxSize uint32 | ||
| } | ||
|
|
||
| type Builder struct { | ||
| payload *v3.CallsPayload | ||
| options BuilderOptions | ||
| malleable []Selector | ||
| repeats []repeatSelector | ||
| } | ||
|
|
||
| type repeatSelector struct { | ||
| a Selector | ||
| b Selector | ||
| } | ||
|
|
||
| type Plan struct { | ||
| Static []StaticSection | ||
| Repeat []RepeatSection | ||
| } | ||
|
|
||
| func (p *Plan) DebugString() string { | ||
| out := "static:" | ||
| for _, s := range p.Static { | ||
| out += fmt.Sprintf(" [t=%d c=%d s=%d]", s.TIndex, s.CIndex, s.Size) | ||
| } | ||
| out += " repeat:" | ||
| for _, r := range p.Repeat { | ||
| out += fmt.Sprintf(" [t=%d c=%d s=%d t2=%d c2=%d]", r.TIndex, r.CIndex, r.Size, r.TIndex2, r.CIndex2) | ||
| } | ||
| return out | ||
| } | ||
|
|
||
| func NewBuilder(payload *v3.CallsPayload, opts *BuilderOptions) *Builder { | ||
| options := BuilderOptions{ | ||
| MaxOffset: 0xFFFF, | ||
| MaxSize: 0xFFFF, | ||
| ValidateRepeats: false, | ||
| MergeAdjacentStatic: true, | ||
| } | ||
| if opts != nil { | ||
| if opts.MaxOffset != 0 { | ||
| options.MaxOffset = opts.MaxOffset | ||
| } | ||
| if opts.MaxSize != 0 { | ||
| options.MaxSize = opts.MaxSize | ||
| } | ||
| options.ValidateRepeats = opts.ValidateRepeats | ||
| options.MergeAdjacentStatic = opts.MergeAdjacentStatic | ||
| } | ||
|
|
||
| return &Builder{ | ||
| payload: payload, | ||
| options: options, | ||
| } | ||
| } | ||
|
|
||
| func (b *Builder) Malleable(sel Selector) *Builder { | ||
| b.malleable = append(b.malleable, sel) | ||
| return b | ||
| } | ||
|
|
||
| func (b *Builder) Repeat(a Selector, b2 Selector) *Builder { | ||
| b.repeats = append(b.repeats, repeatSelector{a: a, b: b2}) | ||
| return b | ||
| } | ||
|
|
||
| func (b *Builder) Build() ([]byte, *Plan, error) { | ||
| if b.payload == nil { | ||
| return nil, nil, fmt.Errorf("payload is nil") | ||
| } | ||
| if len(b.payload.Calls) > 128 { | ||
| return nil, nil, fmt.Errorf("too many calls (%d): tindex is 7-bit", len(b.payload.Calls)) | ||
| } | ||
|
|
||
| exByCall := make([][]Span, len(b.payload.Calls)) | ||
|
|
||
| addExclude := func(r ByteRange) error { | ||
| if r.CallIndex < 0 || r.CallIndex >= len(b.payload.Calls) { | ||
| return fmt.Errorf("tindex out of range: %d", r.CallIndex) | ||
| } | ||
| dataLen := len(b.payload.Calls[r.CallIndex].Data) | ||
| if r.Offset < 0 || r.Size < 0 { | ||
| return fmt.Errorf("span offset/size negative") | ||
| } | ||
| if r.Size > dataLen || r.Offset > dataLen-r.Size { | ||
| return fmt.Errorf("span out of bounds: offset=%d size=%d > %d", r.Offset, r.Size, dataLen) | ||
| } | ||
| exByCall[r.CallIndex] = append(exByCall[r.CallIndex], Span{Start: r.Offset, Len: r.Size}) | ||
| return nil | ||
| } | ||
|
|
||
| for _, sel := range b.malleable { | ||
| ranges, err := sel.Resolve(b.payload) | ||
| if err != nil { | ||
| return nil, nil, fmt.Errorf("malleable %s: %w", sel.String(), err) | ||
| } | ||
| for _, r := range ranges { | ||
| if err := addExclude(r); err != nil { | ||
| return nil, nil, fmt.Errorf("malleable %s: %w", sel.String(), err) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| var repeats []RepeatSection | ||
| for _, rp := range b.repeats { | ||
| rangesA, err := rp.a.Resolve(b.payload) | ||
| if err != nil { | ||
| return nil, nil, fmt.Errorf("repeat a %s: %w", rp.a.String(), err) | ||
| } | ||
| rangesB, err := rp.b.Resolve(b.payload) | ||
| if err != nil { | ||
| return nil, nil, fmt.Errorf("repeat b %s: %w", rp.b.String(), err) | ||
| } | ||
| if len(rangesA) != 1 || len(rangesB) != 1 { | ||
| return nil, nil, fmt.Errorf("repeat expects single range per selector") | ||
| } | ||
| a := rangesA[0] | ||
| bb := rangesB[0] | ||
| if a.Size != bb.Size { | ||
| return nil, nil, fmt.Errorf("repeat size mismatch: %d vs %d", a.Size, bb.Size) | ||
| } | ||
| if err := addExclude(a); err != nil { | ||
| return nil, nil, fmt.Errorf("repeat a: %w", err) | ||
| } | ||
| if err := addExclude(bb); err != nil { | ||
| return nil, nil, fmt.Errorf("repeat b: %w", err) | ||
| } | ||
| if b.options.ValidateRepeats { | ||
| sectionA, err := a.Slice(b.payload) | ||
| if err != nil { | ||
| return nil, nil, err | ||
| } | ||
| sectionB, err := bb.Slice(b.payload) | ||
| if err != nil { | ||
| return nil, nil, err | ||
| } | ||
| if crypto.Keccak256Hash(sectionA) != crypto.Keccak256Hash(sectionB) { | ||
| return nil, nil, fmt.Errorf("repeat section mismatch") | ||
| } | ||
| } | ||
| if a.Offset > maxUint16 || bb.Offset > maxUint16 { | ||
| return nil, nil, fmt.Errorf("repeat offset exceeds uint16 range (max %d): a.Offset=%d b.Offset=%d", maxUint16, a.Offset, bb.Offset) | ||
| } | ||
| if a.Size > maxUint16 { | ||
| return nil, nil, fmt.Errorf("repeat size exceeds uint16 range (max %d): a.Size=%d", maxUint16, a.Size) | ||
| } | ||
| repeats = append(repeats, RepeatSection{ | ||
| TIndex: uint8(a.CallIndex), | ||
| CIndex: uint16(a.Offset), | ||
| Size: uint16(a.Size), | ||
| TIndex2: uint8(bb.CallIndex), | ||
| CIndex2: uint16(bb.Offset), | ||
| }) | ||
| } | ||
|
|
||
| var statics []SpanWithCall | ||
| for t := range b.payload.Calls { | ||
| length := len(b.payload.Calls[t].Data) | ||
| ex := mergeSpans(exByCall[t]) | ||
| cursor := 0 | ||
| for _, s := range ex { | ||
| if cursor < s.Start { | ||
| statics = append(statics, SpanWithCall{CallIndex: t, Span: Span{Start: cursor, Len: s.Start - cursor}}) | ||
| } | ||
| cursor = max(cursor, s.Start+s.Len) | ||
| } | ||
| if cursor < length { | ||
| statics = append(statics, SpanWithCall{CallIndex: t, Span: Span{Start: cursor, Len: length - cursor}}) | ||
| } | ||
| } | ||
|
|
||
| sort.Slice(statics, func(i, j int) bool { | ||
| if statics[i].CallIndex != statics[j].CallIndex { | ||
| return statics[i].CallIndex < statics[j].CallIndex | ||
| } | ||
| return statics[i].Start < statics[j].Start | ||
| }) | ||
|
|
||
| if b.options.MergeAdjacentStatic { | ||
| statics = mergeAdjacentStatics(statics) | ||
| } | ||
|
|
||
| staticSections, err := b.encodeStaticSections(statics) | ||
| if err != nil { | ||
| return nil, nil, err | ||
| } | ||
|
|
||
| signature, err := EncodeSignature(staticSections, repeats) | ||
| if err != nil { | ||
| return nil, nil, err | ||
| } | ||
|
|
||
| return signature, &Plan{Static: staticSections, Repeat: repeats}, nil | ||
| } | ||
|
|
||
| type SpanWithCall struct { | ||
| CallIndex int | ||
| Span | ||
| } | ||
|
|
||
| func (b *Builder) encodeStaticSections(statics []SpanWithCall) ([]StaticSection, error) { | ||
| var sections []StaticSection | ||
| for _, s := range statics { | ||
| if s.Len == 0 { | ||
| continue | ||
| } | ||
| if s.CallIndex < 0 || s.CallIndex >= len(b.payload.Calls) { | ||
| return nil, fmt.Errorf("tindex out of range: %d", s.CallIndex) | ||
| } | ||
| offset := s.Start | ||
| length := s.Len | ||
| for length > 0 { | ||
| chunk := length | ||
| if uint32(chunk) > b.options.MaxSize { | ||
| chunk = int(b.options.MaxSize) | ||
| } | ||
| if uint32(offset) > b.options.MaxOffset { | ||
| return nil, fmt.Errorf("cindex too large: %d", offset) | ||
| } | ||
| if offset > maxUint16 || chunk > maxUint16 { | ||
| return nil, fmt.Errorf("static section offset/size exceeds uint16 range (max %d): offset=%d chunk=%d", maxUint16, offset, chunk) | ||
| } | ||
| sections = append(sections, StaticSection{ | ||
| TIndex: uint8(s.CallIndex), | ||
| CIndex: uint16(offset), | ||
| Size: uint16(chunk), | ||
|
ScreamingHawk marked this conversation as resolved.
|
||
| }) | ||
| offset += chunk | ||
| length -= chunk | ||
| } | ||
| } | ||
| return sections, nil | ||
| } | ||
|
|
||
| func mergeSpans(spans []Span) []Span { | ||
| if len(spans) == 0 { | ||
| return nil | ||
| } | ||
| sort.Slice(spans, func(i, j int) bool { return spans[i].Start < spans[j].Start }) | ||
| out := []Span{spans[0]} | ||
| for _, s := range spans[1:] { | ||
| last := &out[len(out)-1] | ||
| if s.Start <= last.Start+last.Len { | ||
| end := max(last.Start+last.Len, s.Start+s.Len) | ||
| last.Len = end - last.Start | ||
| } else { | ||
| out = append(out, s) | ||
| } | ||
| } | ||
| return out | ||
| } | ||
|
|
||
| func mergeAdjacentStatics(statics []SpanWithCall) []SpanWithCall { | ||
| if len(statics) == 0 { | ||
| return statics | ||
| } | ||
| out := []SpanWithCall{statics[0]} | ||
| for _, s := range statics[1:] { | ||
| last := &out[len(out)-1] | ||
| if s.CallIndex == last.CallIndex && s.Start == last.Start+last.Len { | ||
| last.Len += s.Len | ||
| } else { | ||
| out = append(out, s) | ||
| } | ||
| } | ||
| return out | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.