Skip to content

Commit d7461a4

Browse files
Pad post-task calling context so signer words read the signers column
Post tasks now see the same column layout as order evaluation, so signer<i>() and signed-context<i j>() resolve to the real signers and signed data in deposit4, withdraw4, addOrder4, removeOrder3, entask2 and arb finalize tasks. Closes #2908 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent e3ed129 commit d7461a4

8 files changed

Lines changed: 340 additions & 20 deletions

‎crates/test_fixtures/abis/RaindexV6.json‎

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

‎src/generated/candidate/GenericPoolRaindexV6ArbOrderTaker.sol‎

Lines changed: 4 additions & 4 deletions
Large diffs are not rendered by default.

‎src/generated/candidate/GenericPoolRaindexV6FlashBorrower.sol‎

Lines changed: 4 additions & 4 deletions
Large diffs are not rendered by default.

‎src/generated/candidate/RaindexV6.sol‎

Lines changed: 4 additions & 4 deletions
Large diffs are not rendered by default.

‎src/generated/candidate/RouteProcessorRaindexV6ArbOrderTaker.sol‎

Lines changed: 4 additions & 4 deletions
Large diffs are not rendered by default.

‎src/lib/LibRaindex.sol‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,16 @@ uint256 constant CONTEXT_SIGNED_CONTEXT_START_ROWS = 1;
102102
uint256 constant CONTEXT_SIGNED_CONTEXT_START_ROW = 0;
103103

104104
library LibRaindex {
105-
/// @dev Evaluates each task in `post` against the provided `context`.
106-
/// Tasks with empty bytecode are skipped.
105+
/// @dev Evaluates each task in `post` against `context` padded with empty
106+
/// columns to `CALLING_CONTEXT_COLUMNS`, so the signers and signed context
107+
/// columns are at `CONTEXT_SIGNED_CONTEXT_SIGNERS_COLUMN` and
108+
/// `CONTEXT_SIGNED_CONTEXT_START_COLUMN` as in order evaluation. Tasks with
109+
/// empty bytecode are skipped.
107110
function doPost(bytes32[][] memory context, TaskV2[] memory post) internal {
111+
bytes32[][] memory callingContext = new bytes32[][](CALLING_CONTEXT_COLUMNS);
112+
for (uint256 i = 0; i < context.length; ++i) {
113+
callingContext[i] = context[i];
114+
}
108115
StateNamespace namespace = StateNamespace.wrap(uint256(uint160(msg.sender)));
109116
FullyQualifiedNamespace qualifiedNamespace = LibNamespace.qualifyNamespace(namespace, address(this));
110117
TaskV2 memory task;
@@ -121,7 +128,7 @@ library LibRaindex {
121128
namespace: qualifiedNamespace,
122129
bytecode: task.evaluable.bytecode,
123130
sourceIndex: SourceIndexV2.wrap(0),
124-
context: LibContext.build(context, task.signedContext),
131+
context: LibContext.build(callingContext, task.signedContext),
125132
inputs: emptyStack,
126133
stateOverlay: emptyStateOverlay
127134
})
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
// SPDX-License-Identifier: LicenseRef-DCL-1.0
2+
// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd
3+
pragma solidity =0.8.25;
4+
5+
import {RaindexV6ExternalRealTest} from "test/util/abstract/RaindexV6ExternalRealTest.sol";
6+
import {
7+
OrderConfigV4,
8+
OrderV4,
9+
IOV2,
10+
TaskV2,
11+
EvaluableV4,
12+
SignedContextV1
13+
} from "raindex-interface-0.1.8/src/interface/IRaindexV6.sol";
14+
import {IERC20} from "@openzeppelin-contracts-5.7.0/token/ERC20/IERC20.sol";
15+
import {MessageHashUtils} from "@openzeppelin-contracts-5.7.0/utils/cryptography/MessageHashUtils.sol";
16+
import {Strings} from "@openzeppelin-contracts-5.7.0/utils/Strings.sol";
17+
import {LibDecimalFloat} from "rain-math-float-0.3.10/src/lib/LibDecimalFloat.sol";
18+
import {LibTestAddOrder} from "test/util/lib/LibTestAddOrder.sol";
19+
20+
/// @title RaindexV6PostTaskSignedContextTest
21+
/// @notice `signer<i>()` and `signed-context<i j>()` read the real signers and
22+
/// signed data in every Raindex post task, so a caller cannot forge a signer by
23+
/// placing an address in signed data.
24+
contract RaindexV6PostTaskSignedContextTest is RaindexV6ExternalRealTest {
25+
using Strings for address;
26+
27+
uint256 internal constant SIGNER_A_PK = 0xA11CE;
28+
uint256 internal constant SIGNER_B_PK = 0xB0B;
29+
address internal constant DATA_A = address(0xDA7A0);
30+
address internal constant DATA_B = address(0xDA7A1);
31+
address internal constant TRUSTED = address(0x7777);
32+
address internal constant ACTOR = address(0xD0);
33+
34+
function signed(uint256 pk, bytes32[] memory ctx) internal pure returns (SignedContextV1 memory) {
35+
bytes32 h = MessageHashUtils.toEthSignedMessageHash(keccak256(abi.encodePacked(ctx)));
36+
(uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, h);
37+
return SignedContextV1(vm.addr(pk), ctx, abi.encodePacked(r, s, v));
38+
}
39+
40+
function single(address a) internal pure returns (bytes32[] memory) {
41+
bytes32[] memory ctx = new bytes32[](1);
42+
ctx[0] = bytes32(uint256(uint160(a)));
43+
return ctx;
44+
}
45+
46+
function task(string memory body, SignedContextV1[] memory sc) internal returns (TaskV2[] memory) {
47+
TaskV2[] memory post = new TaskV2[](1);
48+
post[0] = TaskV2(
49+
EvaluableV4(
50+
iInterpreter,
51+
iStore,
52+
iParserV2.parse2(
53+
bytes(string.concat("using-words-from ", address(iSubParser).toHexString(), "\n", body))
54+
)
55+
),
56+
sc
57+
);
58+
return post;
59+
}
60+
61+
/// Two signers, each signing its own data; the task asserts each word reads
62+
/// its own signer and datum.
63+
function realSignersTask() internal returns (TaskV2[] memory) {
64+
SignedContextV1[] memory sc = new SignedContextV1[](2);
65+
sc[0] = signed(SIGNER_A_PK, single(DATA_A));
66+
sc[1] = signed(SIGNER_B_PK, single(DATA_B));
67+
return task(
68+
string.concat(
69+
":ensure(equal-to(signer<0>() ",
70+
vm.addr(SIGNER_A_PK).toHexString(),
71+
") \"signer 0\"),",
72+
":ensure(equal-to(signer<1>() ",
73+
vm.addr(SIGNER_B_PK).toHexString(),
74+
") \"signer 1\"),",
75+
":ensure(equal-to(signed-context<0 0>() ",
76+
DATA_A.toHexString(),
77+
") \"signed context 0\"),",
78+
":ensure(equal-to(signed-context<1 0>() ",
79+
DATA_B.toHexString(),
80+
") \"signed context 1\");"
81+
),
82+
sc
83+
);
84+
}
85+
86+
/// One attacker key signs three contexts, the third holding `TRUSTED`. A
87+
/// task that accepts only `TRUSTED` as signer must reject it.
88+
function forgedSignerTask() internal returns (TaskV2[] memory) {
89+
SignedContextV1[] memory sc = new SignedContextV1[](3);
90+
sc[0] = signed(SIGNER_A_PK, single(DATA_A));
91+
sc[1] = signed(SIGNER_A_PK, single(DATA_A));
92+
sc[2] = signed(SIGNER_A_PK, single(TRUSTED));
93+
return task(string.concat(":ensure(equal-to(signer<0>() ", TRUSTED.toHexString(), ") \"signer trusted\");"), sc);
94+
}
95+
96+
function deposit(TaskV2[] memory post) internal {
97+
vm.mockCall(
98+
address(iToken0),
99+
abi.encodeWithSelector(IERC20.transferFrom.selector, ACTOR, address(iRaindex), 1e18),
100+
abi.encode(true)
101+
);
102+
vm.prank(ACTOR);
103+
iRaindex.deposit4(address(iToken0), bytes32(uint256(1)), LibDecimalFloat.packLossless(1, 0), post);
104+
}
105+
106+
function withdraw(TaskV2[] memory post) internal {
107+
deposit(new TaskV2[](0));
108+
vm.mockCall(address(iToken0), abi.encodeWithSelector(IERC20.transfer.selector, ACTOR, 1e18), abi.encode(true));
109+
vm.prank(ACTOR);
110+
iRaindex.withdraw4(address(iToken0), bytes32(uint256(1)), LibDecimalFloat.packLossless(1, 0), post);
111+
}
112+
113+
function orderConfig() internal view returns (OrderConfigV4 memory) {
114+
IOV2[] memory inputs = new IOV2[](1);
115+
inputs[0] = IOV2(address(iToken0), bytes32(uint256(1)));
116+
IOV2[] memory outputs = new IOV2[](1);
117+
outputs[0] = IOV2(address(iToken1), bytes32(uint256(1)));
118+
return OrderConfigV4(
119+
EvaluableV4(iInterpreter, iStore, LibTestAddOrder.VALID_ORDER_BYTECODE), inputs, outputs, bytes32(0), "", ""
120+
);
121+
}
122+
123+
function addOrder(TaskV2[] memory post) internal {
124+
vm.prank(ACTOR);
125+
assertTrue(iRaindex.addOrder4(orderConfig(), post));
126+
}
127+
128+
function removeOrder(TaskV2[] memory post) internal {
129+
OrderConfigV4 memory config = orderConfig();
130+
vm.prank(ACTOR);
131+
iRaindex.addOrder4(config, new TaskV2[](0));
132+
vm.prank(ACTOR);
133+
assertTrue(
134+
iRaindex.removeOrder3(
135+
OrderV4(ACTOR, config.evaluable, config.validInputs, config.validOutputs, config.nonce), post
136+
)
137+
);
138+
}
139+
140+
function entask(TaskV2[] memory post) internal {
141+
vm.prank(ACTOR);
142+
iRaindex.entask2(post);
143+
}
144+
145+
function testDepositTaskSignerWordsReadRealSigners() external {
146+
deposit(realSignersTask());
147+
}
148+
149+
function testDepositTaskRejectsSignerForgedInSignedData() external {
150+
TaskV2[] memory post = forgedSignerTask();
151+
vm.expectRevert("signer trusted");
152+
this.externalDeposit(post);
153+
}
154+
155+
function testWithdrawTaskSignerWordsReadRealSigners() external {
156+
withdraw(realSignersTask());
157+
}
158+
159+
function testWithdrawTaskRejectsSignerForgedInSignedData() external {
160+
TaskV2[] memory post = forgedSignerTask();
161+
vm.expectRevert("signer trusted");
162+
this.externalWithdraw(post);
163+
}
164+
165+
function testAddOrderTaskSignerWordsReadRealSigners() external {
166+
addOrder(realSignersTask());
167+
}
168+
169+
function testAddOrderTaskRejectsSignerForgedInSignedData() external {
170+
TaskV2[] memory post = forgedSignerTask();
171+
vm.expectRevert("signer trusted");
172+
this.externalAddOrder(post);
173+
}
174+
175+
function testRemoveOrderTaskSignerWordsReadRealSigners() external {
176+
removeOrder(realSignersTask());
177+
}
178+
179+
function testRemoveOrderTaskRejectsSignerForgedInSignedData() external {
180+
TaskV2[] memory post = forgedSignerTask();
181+
vm.expectRevert("signer trusted");
182+
this.externalRemoveOrder(post);
183+
}
184+
185+
function testEntaskSignerWordsReadRealSigners() external {
186+
entask(realSignersTask());
187+
}
188+
189+
function testEntaskRejectsSignerForgedInSignedData() external {
190+
TaskV2[] memory post = forgedSignerTask();
191+
vm.expectRevert("signer trusted");
192+
this.externalEntask(post);
193+
}
194+
195+
function externalDeposit(TaskV2[] memory post) external {
196+
deposit(post);
197+
}
198+
199+
function externalWithdraw(TaskV2[] memory post) external {
200+
withdraw(post);
201+
}
202+
203+
function externalAddOrder(TaskV2[] memory post) external {
204+
addOrder(post);
205+
}
206+
207+
function externalRemoveOrder(TaskV2[] memory post) external {
208+
removeOrder(post);
209+
}
210+
211+
function externalEntask(TaskV2[] memory post) external {
212+
entask(post);
213+
}
214+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// SPDX-License-Identifier: LicenseRef-DCL-1.0
2+
// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd
3+
pragma solidity =0.8.25;
4+
5+
import {Test} from "forge-std-1.17.0/src/Test.sol";
6+
import {EvaluableV4, SignedContextV1, TaskV2} from "raindex-interface-0.1.8/src/interface/IRaindexV6.sol";
7+
import {IInterpreterV4} from "rainlang-interface-0.2.13/src/interface/IInterpreterV4.sol";
8+
import {IInterpreterStoreV3} from "rainlang-interface-0.2.13/src/interface/IInterpreterStoreV3.sol";
9+
import {IParserV2} from "rainlang-interface-0.2.13/src/interface/IParserV2.sol";
10+
import {
11+
LibTestInterpreterDeploy,
12+
TEST_INTERPRETER_ADDRESS,
13+
TEST_STORE_ADDRESS,
14+
TEST_EXPRESSION_DEPLOYER_ADDRESS
15+
} from "rainlang-0.2.19/test/lib/deploy/LibTestInterpreterDeploy.sol";
16+
import {MessageHashUtils} from "@openzeppelin-contracts-5.7.0/utils/cryptography/MessageHashUtils.sol";
17+
import {Strings} from "@openzeppelin-contracts-5.7.0/utils/Strings.sol";
18+
import {LibTestArb} from "test/util/lib/LibTestArb.sol";
19+
import {LibRaindexDeploy} from "src/lib/deploy/LibRaindexDeploy.sol";
20+
import {RUNTIME_CODE as SUB_PARSER_RUNTIME_CODE} from "src/generated/candidate/RaindexV6SubParser.sol";
21+
22+
/// @title LibRaindexArbFinalizeArbTaskSignedContextTest
23+
/// @notice `signer<i>()` and `signed-context<i j>()` read the real signers and
24+
/// signed data in the post-arb task.
25+
contract LibRaindexArbFinalizeArbTaskSignedContextTest is Test {
26+
using Strings for address;
27+
28+
uint256 internal constant SIGNER_A_PK = 0xA11CE;
29+
uint256 internal constant SIGNER_B_PK = 0xB0B;
30+
address internal constant DATA_A = address(0xDA7A0);
31+
address internal constant DATA_B = address(0xDA7A1);
32+
address internal constant TRUSTED = address(0x7777);
33+
34+
function signed(uint256 pk, address datum) internal pure returns (SignedContextV1 memory) {
35+
bytes32[] memory ctx = new bytes32[](1);
36+
ctx[0] = bytes32(uint256(uint160(datum)));
37+
bytes32 h = MessageHashUtils.toEthSignedMessageHash(keccak256(abi.encodePacked(ctx)));
38+
(uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, h);
39+
return SignedContextV1(vm.addr(pk), ctx, abi.encodePacked(r, s, v));
40+
}
41+
42+
function task(string memory body, SignedContextV1[] memory sc) internal returns (TaskV2 memory) {
43+
LibTestInterpreterDeploy.etchTestRainlang(vm);
44+
vm.etch(LibRaindexDeploy.SUB_PARSER_DEPLOYED_ADDRESS, SUB_PARSER_RUNTIME_CODE);
45+
bytes memory bytecode = IParserV2(TEST_EXPRESSION_DEPLOYER_ADDRESS)
46+
.parse2(
47+
bytes(
48+
string.concat(
49+
"using-words-from ", LibRaindexDeploy.SUB_PARSER_DEPLOYED_ADDRESS.toHexString(), "\n", body
50+
)
51+
)
52+
);
53+
return TaskV2({
54+
evaluable: EvaluableV4(
55+
IInterpreterV4(TEST_INTERPRETER_ADDRESS), IInterpreterStoreV3(TEST_STORE_ADDRESS), bytecode
56+
),
57+
signedContext: sc
58+
});
59+
}
60+
61+
function testFinalizeArbTaskSignerWordsReadRealSigners() external {
62+
SignedContextV1[] memory sc = new SignedContextV1[](2);
63+
sc[0] = signed(SIGNER_A_PK, DATA_A);
64+
sc[1] = signed(SIGNER_B_PK, DATA_B);
65+
TaskV2 memory t = task(
66+
string.concat(
67+
":ensure(equal-to(signer<0>() ",
68+
vm.addr(SIGNER_A_PK).toHexString(),
69+
") \"signer 0\"),",
70+
":ensure(equal-to(signer<1>() ",
71+
vm.addr(SIGNER_B_PK).toHexString(),
72+
") \"signer 1\"),",
73+
":ensure(equal-to(signed-context<0 0>() ",
74+
DATA_A.toHexString(),
75+
") \"signed context 0\"),",
76+
":ensure(equal-to(signed-context<1 0>() ",
77+
DATA_B.toHexString(),
78+
") \"signed context 1\");"
79+
),
80+
sc
81+
);
82+
LibTestArb.setupAndArb(vm, 80e18, 100e18, 100e18, 100e18, t, 0);
83+
}
84+
85+
function testFinalizeArbTaskRejectsSignerForgedInSignedData() external {
86+
SignedContextV1[] memory sc = new SignedContextV1[](3);
87+
sc[0] = signed(SIGNER_A_PK, DATA_A);
88+
sc[1] = signed(SIGNER_A_PK, DATA_A);
89+
sc[2] = signed(SIGNER_A_PK, TRUSTED);
90+
TaskV2 memory t =
91+
task(string.concat(":ensure(equal-to(signer<0>() ", TRUSTED.toHexString(), ") \"signer trusted\");"), sc);
92+
vm.expectRevert("signer trusted");
93+
this.externalArb(t);
94+
}
95+
96+
function externalArb(TaskV2 memory t) external {
97+
LibTestArb.setupAndArb(vm, 80e18, 100e18, 100e18, 100e18, t, 0);
98+
}
99+
}

0 commit comments

Comments
 (0)