Skip to content

Commit 3241ee2

Browse files
Add hydrate lib (#353)
* Move abicalldata to own package * Add hydrate lib * Fix address offset * bytes32 size validation
1 parent a754d32 commit 3241ee2

15 files changed

Lines changed: 1260 additions & 76 deletions

File tree

lib/abicalldata/README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# ABI calldata
2+
3+
Helpers for **Ethereum ABI-encoded calldata**: head layout, static argument words, and dynamic `bytes`/`string` tails. Also defines **`ByteRange`** and **`Selector`** for locating byte spans inside a v3 **`CallsPayload`**.
4+
5+
## Calldata layout
6+
7+
Offsets are **from the start of calldata** (bytes `0..3` are the 4-byte function selector).
8+
9+
- **`AbiHeadWords(t)`** — number of 32-byte words an ABI type occupies in the **head** (dynamic types count as one offset word).
10+
- **`CalldataStaticWord(method, argIndex)`**`start` and `length` (multiple of 32) for a **static** argument’s encoded words in `calldata`.
11+
- **`CalldataBytesContent(calldata, method, argIndex)`**`start`/`length` of the **raw** `bytes`/`string` payload (no length word, no tail padding).
12+
- **`CalldataBytesEncoded(calldata, method, argIndex)`**`start`/`length` of the full **tail slice**: 32-byte length word + content + padding to 32-byte boundary.
13+
14+
```go
15+
method := contractABI.Methods["transfer"]
16+
start, length, err := abicalldata.CalldataStaticWord(method, 1) // e.g. uint256 amount
17+
if err != nil {
18+
return err
19+
}
20+
argBytes := calldata[start : start+length]
21+
```
22+
23+
For nested calldata (e.g. a `bytes` argument whose body is another contract call), slice to the inner calldata and call these helpers again with the inner method’s `abi.Method`.
24+
25+
## Calls payload selectors
26+
27+
- **`ByteRange`**`{CallIndex, Offset, Size}` into `payload.Calls[CallIndex].Data` (that field is the call’s calldata, typically including the inner 4-byte selector).
28+
- **`Selector`**`Resolve(*v3.CallsPayload) ([]ByteRange, error)` plus `String()` for errors.
29+
- **`NewRangeSelector(callIndex, offset, size)`** — fixed range, validated on resolve.
30+
31+
```go
32+
sel := abicalldata.NewRangeSelector(0, 0x24, 32)
33+
ranges, err := sel.Resolve(payload)
34+
```
35+
36+
**`Selector`** is implemented by types in other packages that walk `CallsPayload` and nested calldata; they typically use the functions above to turn `abi.Method` + argument index into `ByteRange` updates. **`NewRangeSelector`** is the built-in implementation for a fixed call index, offset, and length.
Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package malleable
1+
package abicalldata
22

33
import (
44
"fmt"
@@ -8,25 +8,25 @@ import (
88
"github.com/0xsequence/ethkit/go-ethereum/accounts/abi"
99
)
1010

11-
// abiHeadWords returns the number of 32-byte words this type occupies in the
11+
// AbiHeadWords returns the number of 32-byte words this type occupies in the
1212
// ABI calldata head. Dynamic types (bytes, string, slice, and tuples/arrays
1313
// that contain them) occupy 1 word (offset); static types use their encoded size.
14-
func abiHeadWords(t abi.Type) int {
14+
func AbiHeadWords(t abi.Type) int {
1515
switch t.T {
1616
case abi.BytesTy, abi.StringTy, abi.SliceTy:
1717
return 1
1818
case abi.ArrayTy:
1919
if t.Size == 0 || isDynamicType(t) {
2020
return 1
2121
}
22-
return t.Size * abiHeadWords(*t.Elem)
22+
return t.Size * AbiHeadWords(*t.Elem)
2323
case abi.TupleTy:
2424
if isDynamicType(t) {
2525
return 1
2626
}
2727
n := 0
2828
for _, e := range t.TupleElems {
29-
n += abiHeadWords(*e)
29+
n += AbiHeadWords(*e)
3030
}
3131
return n
3232
default:
@@ -43,7 +43,7 @@ func calldataArgHeadOffset(method abi.Method, argIndex int) (int, error) {
4343
}
4444
offset := 4
4545
for j := 0; j < argIndex; j++ {
46-
offset += abiHeadWords(method.Inputs[j].Type) * 32
46+
offset += AbiHeadWords(method.Inputs[j].Type) * 32
4747
}
4848
return offset, nil
4949
}
@@ -53,7 +53,7 @@ func CalldataStaticWord(method abi.Method, argIndex int) (start, length int, err
5353
if err != nil {
5454
return 0, 0, err
5555
}
56-
words := abiHeadWords(method.Inputs[argIndex].Type)
56+
words := AbiHeadWords(method.Inputs[argIndex].Type)
5757
return start, words * 32, nil
5858
}
5959

@@ -123,3 +123,21 @@ func calldataBytesTail(calldata []byte, method abi.Method, argIndex int) (tailSt
123123
}
124124
return tailStart, dataLen, nil
125125
}
126+
127+
func isDynamicType(t abi.Type) bool {
128+
switch t.T {
129+
case abi.BytesTy, abi.StringTy, abi.SliceTy:
130+
return true
131+
case abi.ArrayTy:
132+
return t.Size == 0 || isDynamicType(*t.Elem)
133+
case abi.TupleTy:
134+
for _, elem := range t.TupleElems {
135+
if isDynamicType(*elem) {
136+
return true
137+
}
138+
}
139+
return false
140+
default:
141+
return false
142+
}
143+
}

lib/sapient/malleable/locators_abi_test.go renamed to lib/abicalldata/calldata_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package malleable
1+
package abicalldata
22

33
import (
44
"math"
Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,19 @@
1-
package malleable
1+
package abicalldata
22

33
import (
44
"fmt"
55

66
v3 "github.com/0xsequence/go-sequence/core/v3"
77
)
88

9-
type Span struct {
10-
Start int
11-
Len int
12-
}
13-
9+
// ByteRange identifies a contiguous slice of one call's calldata (data field) in a CallsPayload.
1410
type ByteRange struct {
1511
CallIndex int
1612
Offset int
1713
Size int
1814
}
1915

16+
// Slice returns the referenced bytes from payload.Calls[CallIndex].Data.
2017
func (r ByteRange) Slice(payload *v3.CallsPayload) ([]byte, error) {
2118
if payload == nil {
2219
return nil, fmt.Errorf("payload is nil")
@@ -34,10 +31,18 @@ func (r ByteRange) Slice(payload *v3.CallsPayload) ([]byte, error) {
3431
return data[r.Offset : r.Offset+r.Size], nil
3532
}
3633

34+
// Selector resolves one or more byte ranges in a calls payload (e.g. ABI paths or fixed ranges).
35+
type Selector interface {
36+
Resolve(payload *v3.CallsPayload) ([]ByteRange, error)
37+
String() string
38+
}
39+
40+
// RangeSelector is a Selector backed by a fixed ByteRange (validated on Resolve).
3741
type RangeSelector struct {
3842
Range ByteRange
3943
}
4044

45+
// NewRangeSelector returns a Selector for an explicit call index, offset, and size within that call's data.
4146
func NewRangeSelector(callIndex, offset, size int) RangeSelector {
4247
return RangeSelector{
4348
Range: ByteRange{

lib/hydrate/README.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Hydrate
2+
3+
Build `hydratePayload` bytes for **HydrateProxy** (`hydrateExecute` / `hydrateExecuteAndSweep` in trails-contracts) by resolving patch targets with [`abicalldata`](https://github.com/0xsequence/go-sequence/tree/master/lib/abicalldata) **`Selector`** and **`ByteRange`** instead of hand-counting calldata offsets.
4+
5+
`hydrate` depends only on `abicalldata` for selector types. The example below builds an `abicalldata.Selector` using `Path` from `lib/sapient/malleable`; fixed offsets can use `abicalldata.NewRangeSelector` instead.
6+
7+
## Usage
8+
9+
```go
10+
payload := v3.NewCallsPayload(...)
11+
12+
// Selector must resolve to exactly one range on the target call (same index as ForCall).
13+
permitOwner := malleable.NewPath().
14+
CallData(0).
15+
ABI(trailsABI, "hydrateExecute").
16+
ArgBytesData("packedPayload").
17+
EncodedCallsPayload().
18+
EncodedCallData(0).
19+
ABI(erc2612ABI, "permit").
20+
ArgSlot("owner").
21+
AsSelector()
22+
23+
b := hydrate.NewBuilder(&payload)
24+
25+
if err := b.ForCall(0).DataAddress(permitOwner, hydrate.SourceSelf()); err != nil {
26+
return err
27+
}
28+
29+
hydratePayload, err := b.Build()
30+
if err != nil {
31+
return err
32+
}
33+
// nil/empty hydratePayload means "no hydration" (contract skips hydration).
34+
35+
calldata, err := hydrate.PackHydrateExecute(packedPayload, hydratePayload)
36+
```
37+
38+
With sweep after execution:
39+
40+
```go
41+
calldata, err := hydrate.PackHydrateExecuteAndSweep(
42+
packedPayload,
43+
hydratePayload,
44+
sweepTarget,
45+
tokensToSweep,
46+
sweepNative,
47+
)
48+
```
49+
50+
If ABI parameters are unnamed, use index-based steps such as `ArgSlotIndex` / `ArgBytesDataIndex` when your path builder provides them.
51+
52+
## Call sections and ordering
53+
54+
- Use `ForCall(tindex)` for each packed call you want to hydrate. Multiple methods on the same `ForCall` append commands to that call's section.
55+
- `Build()` emits sections in **ascending** `tindex` order, each as: `[tindex byte][commands…][0x00]`. That order matches how the proxy walks the stream while executing calls; sections must not be reordered arbitrarily.
56+
57+
## Address sources
58+
59+
Data patches and `CallTo` / `CallValue` use a low nibble on the command byte; optional literals append 20 bytes when needed:
60+
61+
| Helper | Meaning at execution time |
62+
|--------|---------------------------|
63+
| `SourceSelf()` | `address(this)` (the proxy) |
64+
| `SourceMsgSender()` | `msg.sender` |
65+
| `SourceTxOrigin()` | `tx.origin` |
66+
| `SourceAddress(a)` | fixed `a` (encoded after the flag) |
67+
68+
## Selectors
69+
70+
Each `Data*` method requires an `abicalldata.Selector` that resolves to **exactly one** `abicalldata.ByteRange`, and that range's `CallIndex` must equal the `ForCall` index. The range's offset within that call's `data` becomes the patch offset (`uint16`); the builder checks that 20-byte (address) or 32-byte (uint256) replacements fit.
71+
72+
For unit tests or fixed layouts, `abicalldata.NewRangeSelector(callIndex, offset, size)` returns a selector backed by an explicit range.
73+
74+
When using a multi-step path into nested calldata, ABI context is cleared after any step that narrows the active range or switches to a new byte frame (including `.Slice`, `.ArgBytesData`, `.ArgBytesDataIndex`, `.ArgBytesEncoded`, `.EncodedCallsPayload`, `.EncodedCallData`). Call `.ABI(contractABI, method)` again before `.ArgSlot`, `.ArgSlotIndex`, `.ArgBytesData`, `.ArgBytesDataIndex`, or `.ArgBytesEncoded` on that frame.

lib/hydrate/abi.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package hydrate
2+
3+
import (
4+
"bytes"
5+
_ "embed"
6+
"fmt"
7+
8+
"github.com/0xsequence/ethkit/go-ethereum/accounts/abi"
9+
"github.com/0xsequence/ethkit/go-ethereum/common"
10+
)
11+
12+
//go:embed abis/HydrateProxy.abi.json
13+
var hydrateProxyABIJSON []byte
14+
15+
// ABI is the parsed HydrateProxy contract ABI (from trails-contracts artifact).
16+
var ABI abi.ABI
17+
18+
func init() {
19+
var err error
20+
ABI, err = abi.JSON(bytes.NewReader(hydrateProxyABIJSON))
21+
if err != nil {
22+
panic(fmt.Sprintf("hydrate: parse embedded HydrateProxy ABI: %v", err))
23+
}
24+
}
25+
26+
// PackHydrateExecute ABI-encodes a call to HydrateProxy.hydrateExecute.
27+
func PackHydrateExecute(packedPayload, hydratePayload []byte) ([]byte, error) {
28+
return ABI.Pack("hydrateExecute", packedPayload, hydratePayload)
29+
}
30+
31+
// PackHydrateExecuteAndSweep ABI-encodes hydrateExecuteAndSweep.
32+
func PackHydrateExecuteAndSweep(packedPayload, hydratePayload []byte, sweepTarget common.Address, tokensToSweep []common.Address, sweepNative bool) ([]byte, error) {
33+
return ABI.Pack("hydrateExecuteAndSweep", packedPayload, hydratePayload, sweepTarget, tokensToSweep, sweepNative)
34+
}

0 commit comments

Comments
 (0)