Skip to content

Commit

Permalink
Feat/fill OTC Order with Permit (#186)
Browse files Browse the repository at this point in the history
* constructFillOrderDirectly/allow for takerPermit

* constructApproveTokenForLimitOrder/approveTakerTokenForFillingP2POrderDirectly

* test helpers/extends buyErc20TokenForEth

* tests/fix comments

* tests/LO/fill OTC directly

* tests/LO/more helpers

* tests/LO/fill OTC directly with Permit

* helpers/splitSignature

* helper/encodeEIP_2612PermitFunctionInput

* fillOrderDirectly/handle EIP2612 takerPermit

* test/LO/OTC with Permit

* helpers/encodeDAIlikePermitFunctionInput

* lOrder/fillOrderDirectly/handle DAI Permit and pre-encoded permit params

* test/LOrder/fill OTC with DAI Permit

* cleanup

* test/LO/reenable tests

* update snapshots
  • Loading branch information
Velenir authored Dec 3, 2024
1 parent 17c2c21 commit 7b637bc
Show file tree
Hide file tree
Showing 8 changed files with 1,289 additions and 51 deletions.
216 changes: 216 additions & 0 deletions src/methods/common/orders/encoding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import { splitSignature } from './signature';

type EncodeEIP_2612PermitFunctionInput = {
permitSignature: string;
owner: string;
spender: string;
value: string | bigint;
deadline: string | number | bigint;
};

// encoding params for Token.permit() Permit1 function
export function encodeEIP_2612PermitFunctionInput({
owner,
spender,
value,
deadline,
permitSignature,
}: EncodeEIP_2612PermitFunctionInput): string {
const { v, r, s } = splitSignature(permitSignature);

const encodedOwner = encodeAddress(owner);
const encodedSpender = encodeAddress(spender);
const encodedValue = encodeUint256(value);
const encodedDeadline = encodeUint256(deadline.toString());
const encodedV = encodeUint8(v);
const encodedR = encodeBytes32(r);
const encodedS = encodeBytes32(s);

// Concatenate all encoded values, stripping the "0x" prefix from each (except the first one)
return (
'0x' +
[
encodedOwner,
encodedSpender,
encodedValue,
encodedDeadline,
encodedV,
encodedR,
encodedS,
]
.map((val) => val.slice(2)) // Remove "0x" prefix from each encoded value
.join('') // Concatenate the values
);
}

type EncodeDAIlikePermitFunctionInput = {
permitSignature: string;
holder: string;
spender: string;
nonce: number | bigint | string;
expiry: number | bigint | string;
};

// encoding params for DAIlike.permit() function
export function encodeDAIlikePermitFunctionInput({
permitSignature,
holder,
spender,
nonce,
expiry,
}: EncodeDAIlikePermitFunctionInput): string {
const { v, r, s } = splitSignature(permitSignature);

const encodedHolder = encodeAddress(holder);
const encodedSpender = encodeAddress(spender);
const encodedNonce = encodeUint256(nonce.toString());
const encodedExpiry = encodeUint256(expiry.toString());
const encodedV = encodeUint8(v);
const encodedR = encodeBytes32(r);
const encodedS = encodeBytes32(s);

// Concatenate all encoded values, stripping the "0x" prefix from each (except the first one)
return (
'0x' +
[
encodedHolder,
encodedSpender,
encodedNonce,
encodedExpiry,
encodeBool(true), //allowed=true
encodedV,
encodedR,
encodedS,
]
.map((val) => val.slice(2)) // Remove "0x" prefix from each encoded value
.join('') // Concatenate the values
);
}

// encode an address (20 bytes) into 32 bytes
export function encodeAddress(address: string): string {
const strippedAddress = address.replace(/^0x/, ''); // Remove "0x" prefix
return '0x' + strippedAddress.toLowerCase().padStart(64, '0');
}

// encode a uint256 value
export function encodeUint256(value: string | bigint): string {
const bn = BigInt(value);
return '0x' + bn.toString(16).padStart(64, '0');
}

// encode a uint8 value
export function encodeUint8(value: number | bigint): string {
return '0x' + value.toString(16).padStart(64, '0');
}

// encode a bytes32 value
export function encodeBytes32(value: string): string {
const strippedValue = value.replace(/^0x/, ''); // Remove "0x" prefix
return '0x' + strippedValue.padStart(64, '0').toLowerCase();
}

//encode a boolean
export function encodeBool(value: boolean): string {
const encodedValue = value ? '1' : '0';
// padded to 32 bytes
return '0x' + encodedValue.padStart(64, '0');
}

/*
const EIP_2612_PERMIT_ABI = [
{
constant: false,
inputs: [
{
name: 'owner',
type: 'address',
},
{
name: 'spender',
type: 'address',
},
{
name: 'value',
type: 'uint256',
},
{
name: 'deadline',
type: 'uint256',
},
{
name: 'v',
type: 'uint8',
},
{
name: 'r',
type: 'bytes32',
},
{
name: 's',
type: 'bytes32',
},
],
name: 'permit',
outputs: [],
payable: false,
stateMutability: 'nonpayable',
type: 'function',
},
];
*/

/*
const DAI_EIP_2612_PERMIT_ABI = [
{
constant: false,
inputs: [
{
internalType: 'address',
name: 'holder',
type: 'address',
},
{
internalType: 'address',
name: 'spender',
type: 'address',
},
{
internalType: 'uint256',
name: 'nonce',
type: 'uint256',
},
{
internalType: 'uint256',
name: 'expiry',
type: 'uint256',
},
{
internalType: 'bool',
name: 'allowed',
type: 'bool',
},
{
internalType: 'uint8',
name: 'v',
type: 'uint8',
},
{
internalType: 'bytes32',
name: 'r',
type: 'bytes32',
},
{
internalType: 'bytes32',
name: 's',
type: 'bytes32',
},
],
name: 'permit',
outputs: [],
payable: false,
stateMutability: 'nonpayable',
type: 'function',
},
];
*/
68 changes: 68 additions & 0 deletions src/methods/common/orders/signature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
type SplitSignatureResult = {
v: number;
r: string;
s: string;
compact: string;
};

export function splitSignature(signature: string): SplitSignatureResult {
// Remove "0x" prefix if present
if (signature.startsWith('0x')) {
signature = signature.slice(2);
}

// Convert the hex string to a byte array
const bytes = new Uint8Array(signature.length / 2);
for (let i = 0; i < signature.length; i += 2) {
bytes[i / 2] = parseInt(signature.slice(i, i + 2), 16);
}

// Validate the signature length (64 or 65 bytes)
if (bytes.length !== 64 && bytes.length !== 65) {
throw new Error('Invalid signature length: must be 64 or 65 bytes');
}

// Extract r and s components
const r = `0x${Array.from(bytes.slice(0, 32), (b) =>
b.toString(16).padStart(2, '0')
).join('')}`;
let s: string;
let v;

// Handle 64-byte (EIP-2098 compact) and 65-byte signatures
if (bytes.length === 64) {
// Extract v from the highest bit of s and clear the bit in s
v = 27 + (bytes[32]! >> 7);
bytes[32]! &= 0x7f; // Clear the highest bit
s = `0x${Array.from(bytes.slice(32, 64))
.map((b) => b.toString(16).padStart(2, '0'))
.join('')}`;
} else {
s = `0x${Array.from(bytes.slice(32, 64))
.map((b) => b.toString(16).padStart(2, '0'))
.join('')}`;

// Extract v directly for 65-byte signature
v = bytes[64]!;

// Normalize v to canonical form (27 or 28)
if (v < 27) {
v += 27;
}
}

// Compute yParityAndS (_vs) for the compact signature
const sBytes = Array.from(bytes.slice(32, 64));
if (v === 28) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
sBytes[0]! |= 0x80; // Set the highest bit if v is 28
}
const yParityAndS = `0x${sBytes
.map((b) => b.toString(16).padStart(2, '0'))
.join('')}`;

// Construct the compact signature by concatenating r and yParityAndS
const compactSignature = r + yParityAndS.slice(2);

return { v, r, s, compact: compactSignature };
}
3 changes: 3 additions & 0 deletions src/methods/limitOrders/approveForOrder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { constructGetSpender } from '../swap/spender';
export type ApproveTokenForLimitOrderFunctions<T> = {
/** @description approving AugustusRFQ as spender for makerAsset */
approveMakerTokenForLimitOrder: ApproveToken<T>;
/** @description approving AugustusRFQ as spender for takerAsset to call SDK.fillOrderDirectly */
approveTakerTokenForFillingP2POrderDirectly: ApproveToken<T>;
/** @description approving AugustusSwapper as spender for takerAsset for Limit Orders that will be executed through it */
approveTakerTokenForLimitOrder: ApproveToken<T>;
};
Expand All @@ -28,6 +30,7 @@ export const constructApproveTokenForLimitOrder = <T>(

return {
approveMakerTokenForLimitOrder,
approveTakerTokenForFillingP2POrderDirectly: approveMakerTokenForLimitOrder,
approveTakerTokenForLimitOrder,
};
};
Loading

0 comments on commit 7b637bc

Please sign in to comment.