Skip to content

Commit 9595261

Browse files
committed
Added multiple lessons about contracts deploying other contracts and parent-child
1 parent 2c4a711 commit 9595261

File tree

28 files changed

+1120
-3
lines changed

28 files changed

+1120
-3
lines changed

src/routes/(examples)/+layout.svelte

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,49 @@
6262
`Transaction executed: ${compute.success ? "success" : "error"}, ` +
6363
`exit code ${compute.exitCode}, gas ${shorten(compute.gasFees, "coins")}`,
6464
);
65+
let foundError = false;
6566
for (const contractInstance of contractInstances) {
6667
if (transaction.inMessage?.info.dest.equals(contractInstance.address)) {
6768
if (compute.exitCode == -14) compute.exitCode = 13;
6869
const message = contractInstance?.abi?.errors?.[compute.exitCode]?.message;
69-
if (message) terminalLog(`Error message: ${message}`);
70+
if (message) {
71+
terminalLog(`Error message: ${message}`);
72+
foundError = true;
73+
}
74+
}
75+
}
76+
if (!foundError) {
77+
const knownErrors: { [code: number]: { message: string } } = {
78+
[-14]: { message: `Out of gas error` },
79+
2: { message: `Stack undeflow` },
80+
3: { message: `Stack overflow` },
81+
4: { message: `Integer overflow` },
82+
5: { message: `Integer out of expected range` },
83+
6: { message: `Invalid opcode` },
84+
7: { message: `Type check error` },
85+
8: { message: `Cell overflow` },
86+
9: { message: `Cell underflow` },
87+
10: { message: `Dictionary error` },
88+
13: { message: `Out of gas error` },
89+
32: { message: `Method ID not found` },
90+
34: { message: `Action is invalid or not supported` },
91+
37: { message: `Not enough TON` },
92+
38: { message: `Not enough extra-currencies` },
93+
128: { message: `Null reference exception` },
94+
129: { message: `Invalid serialization prefix` },
95+
130: { message: `Invalid incoming message` },
96+
131: { message: `Constraints error` },
97+
132: { message: `Access denied` },
98+
133: { message: `Contract stopped` },
99+
134: { message: `Invalid argument` },
100+
135: { message: `Code of a contract was not found` },
101+
136: { message: `Invalid address` },
102+
137: { message: `Masterchain support is not enabled for this contract` },
103+
};
104+
const message = knownErrors[compute.exitCode]?.message;
105+
if (message) {
106+
terminalLog(`Error message: ${message}`);
107+
foundError = true;
70108
}
71109
}
72110
}
@@ -99,6 +137,9 @@
99137
if (op == type.header) return type.name;
100138
}
101139
}
140+
if (op == 0xffffffff) {
141+
return "error";
142+
}
102143
return `unknown (0x${op.toString(16)})`;
103144
} catch (e) {}
104145
return "empty";
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<script lang="ts">
2+
import { toNano, type Sender } from "ton-core";
3+
import { Blockchain, type SandboxContract } from "@ton-community/sandbox";
4+
import { getExample } from "$lib/helpers";
5+
import store from "$lib/store";
6+
7+
import markdown from "./content.md?raw";
8+
import tactCode from "./contract.tact?raw";
9+
import { TodoParent } from "./TodoParent";
10+
import { TodoChild } from "./TodoChild";
11+
12+
let blockchain: Blockchain;
13+
let sender: Sender;
14+
let contract: SandboxContract<TodoParent>;
15+
16+
$store = {
17+
markdown,
18+
tactCode,
19+
deploy: async () => {
20+
blockchain = await Blockchain.create();
21+
const deployer = await blockchain.treasury("deployer");
22+
sender = deployer.getSender();
23+
contract = blockchain.openContract(await TodoParent.fromInit());
24+
const addresses = {
25+
[deployer.address.toString()]: "deployer",
26+
[contract.address.toString()]: "TodoParent",
27+
[(await TodoChild.fromInit(contract.address, 1n)).address.toString()]: "TodoChild(1)",
28+
[(await TodoChild.fromInit(contract.address, 2n)).address.toString()]: "TodoChild(2)",
29+
[(await TodoChild.fromInit(contract.address, 3n)).address.toString()]: "TodoChild(3)",
30+
};
31+
return [[contract], addresses, [await contract.send(deployer.getSender(), { value: toNano(1) }, { $$type: "Deploy", queryId: 0n })]];
32+
},
33+
messages: {
34+
"greet 3": async () => {
35+
return [await contract.send(sender, { value: toNano(1) }, "greet 3")];
36+
},
37+
"HiFromChild{1}": async () => {
38+
return [await contract.send(sender, { value: toNano(1) }, { $$type: "HiFromChild", fromSeqno: 1n, greeting: "hack" })];
39+
},
40+
},
41+
getters: {},
42+
prev: getExample(import.meta.url).prev,
43+
next: getExample(import.meta.url).next,
44+
};
45+
</script>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Authenticating Children
2+
3+
If you look closely at the previous example you will notice that it is somewhat dangerous.
4+
5+
What happens if some user tries to send a `HiFromChild` message and impersonate a child? What happens if some user tries to send a `HiFromParent` message and impersonate the parent?
6+
7+
To add authentication that messages came from where we think they came from, we simply need to add `require()` in the beginning of every protected receiver and make sure that the sender is who we expect it is.
8+
9+
It is best practice to add this authentication to every message coming from a parent and every message coming from a child.
10+
11+
## Let's try to hack this contract
12+
13+
Try pressing the <span class="mdButton grape">Send HiFromChild{1}</span> button. This will send the parent an impersonated `HiFromChild` message, but from some user, not from a real child.
14+
15+
Since this code is now protected, it will notice that the sender is incorrect and reject the message with an access denied error.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import "@stdlib/deploy";
2+
3+
message HiFromParent {
4+
greeting: String;
5+
}
6+
7+
message HiFromChild {
8+
fromSeqno: Int as uint64;
9+
greeting: String;
10+
}
11+
12+
// we have multiple instances of the children
13+
contract TodoChild {
14+
15+
parent: Address; // we added this variable so a child always knows who the parent is
16+
seqno: Int as uint64;
17+
18+
// when deploying an instance, we must specify its index (sequence number)
19+
init(parent: Address, seqno: Int) {
20+
self.parent = parent;
21+
self.seqno = seqno;
22+
}
23+
24+
receive(msg: HiFromParent) {
25+
require(sender() == self.parent, "Access denied");
26+
// only the real parent can get here
27+
dump(self.seqno);
28+
dump("handling hi from parent");
29+
reply(HiFromChild{fromSeqno: self.seqno, greeting: "sup"}.toCell());
30+
}
31+
}
32+
33+
// we have one instance of the parent
34+
contract TodoParent with Deployable {
35+
36+
init() {}
37+
38+
receive("greet 3") {
39+
let i: Int = 0;
40+
repeat (3) {
41+
i = i + 1;
42+
let init: StateInit = initOf TodoChild(myAddress(), i);
43+
send(SendParameters{
44+
to: contractAddress(init),
45+
body: HiFromParent{greeting: "darling"}.toCell(),
46+
value: ton("0.1"), // pay for message and potential deployment
47+
mode: SendIgnoreErrors,
48+
code: init.code, // if child is not deployed, also deploy it
49+
data: init.data
50+
});
51+
}
52+
}
53+
54+
receive(msg: HiFromChild) {
55+
let expectedAddress: Address = contractAddress(initOf TodoChild(myAddress(), msg.fromSeqno));
56+
require(sender() == expectedAddress, "Access denied");
57+
// only the real children can get here
58+
dump("handling hi from child");
59+
dump(msg.fromSeqno);
60+
}
61+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<script lang="ts">
2+
import { toNano, type Sender } from "ton-core";
3+
import { Blockchain, type SandboxContract } from "@ton-community/sandbox";
4+
import { getExample } from "$lib/helpers";
5+
import store from "$lib/store";
6+
7+
import markdown from "./content.md?raw";
8+
import tactCode from "./contract.tact?raw";
9+
import { Todo1 } from "./Todo1";
10+
import { Todo2 } from "./Todo2";
11+
12+
let sender: Sender;
13+
let contract: SandboxContract<Todo1>;
14+
let contract2: SandboxContract<Todo2>;
15+
16+
$store = {
17+
markdown,
18+
tactCode,
19+
deploy: async () => {
20+
const blockchain = await Blockchain.create();
21+
const deployer = await blockchain.treasury("deployer");
22+
sender = deployer.getSender();
23+
contract = blockchain.openContract(await Todo1.fromInit());
24+
contract2 = blockchain.openContract(await Todo2.fromInit());
25+
const addresses = {
26+
[deployer.address.toString()]: "deployer",
27+
[contract.address.toString()]: "Todo1",
28+
[contract2.address.toString()]: "Todo2",
29+
};
30+
return [
31+
[contract, contract2],
32+
addresses,
33+
[
34+
await contract.send(deployer.getSender(), { value: toNano(1) }, { $$type: "Deploy", queryId: 0n }),
35+
await contract2.send(deployer.getSender(), { value: toNano(1) }, { $$type: "Deploy", queryId: 0n }),
36+
],
37+
];
38+
},
39+
messages: {},
40+
getters: {
41+
"Todo1.myAddress": async () => {
42+
return await contract.getMyAddress();
43+
},
44+
"Todo1.otherAddress": async () => {
45+
return await contract.getOtherAddress();
46+
},
47+
"Todo2.myAddress": async () => {
48+
return await contract2.getMyAddress();
49+
},
50+
"Todo2.otherAddress": async () => {
51+
return await contract2.getOtherAddress();
52+
},
53+
},
54+
prev: getExample(import.meta.url).prev,
55+
next: getExample(import.meta.url).next,
56+
};
57+
</script>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Calculate Contract Address
2+
3+
When a contract is deployed to the chain, it receives an address by which users can refer to it and send it transactions.
4+
5+
In this example, we have two different contracts: `Todo1` and `Todo2`. They are deployed separately and each gets its own unique address. As we've seen before, a contract can always know its own address by running `myAddress()`.
6+
7+
The special bit in this example is that each contract can also get the address of the other contract by running `contractAddress(stateInit)`.
8+
9+
## How is the contract address generated?
10+
11+
Contract addresses on TON are [derived](https://docs.ton.org/learn/overviews/addresses#account-id) from the initial code of the contract (the compiled bytecode) and the initial data of the contract (the arguments of init).
12+
13+
Both contracts don't have any constructor arguments, so their initial data is the identical. Their addresses are different because their code is different.
14+
15+
The combination of the inital code and the initial data is called the *stateInit* of the contract. Tact gives easy access to the *stateInit* using the `initOf` statement.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import "@stdlib/deploy";
2+
3+
// first contract
4+
contract Todo1 with Deployable {
5+
6+
seqno: Int as uint64 = 1; // the code specifies the index (sequence number)
7+
8+
init() {}
9+
10+
get fun myAddress(): Address {
11+
return myAddress();
12+
}
13+
14+
get fun otherAddress(): Address {
15+
let init: StateInit = initOf Todo2();
16+
return contractAddress(init);
17+
}
18+
}
19+
20+
// second contract
21+
contract Todo2 with Deployable {
22+
23+
seqno: Int as uint64 = 2; // the code specifies the index (sequence number)
24+
25+
init() {}
26+
27+
get fun myAddress(): Address {
28+
return myAddress();
29+
}
30+
31+
get fun otherAddress(): Address {
32+
let init: StateInit = initOf Todo1();
33+
return contractAddress(init);
34+
}
35+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<script lang="ts">
2+
import { toNano, type Sender } from "ton-core";
3+
import { Blockchain, type SandboxContract } from "@ton-community/sandbox";
4+
import { getExample } from "$lib/helpers";
5+
import store from "$lib/store";
6+
7+
import markdown from "./content.md?raw";
8+
import tactCode from "./contract.tact?raw";
9+
import { TodoParent } from "./TodoParent";
10+
import { TodoChild } from "./TodoChild";
11+
12+
let blockchain: Blockchain;
13+
let sender: Sender;
14+
let contract: SandboxContract<TodoParent>;
15+
16+
$store = {
17+
markdown,
18+
tactCode,
19+
deploy: async () => {
20+
blockchain = await Blockchain.create();
21+
const deployer = await blockchain.treasury("deployer");
22+
sender = deployer.getSender();
23+
contract = blockchain.openContract(await TodoParent.fromInit());
24+
const addresses = {
25+
[deployer.address.toString()]: "deployer",
26+
[contract.address.toString()]: "TodoParent",
27+
[(await TodoChild.fromInit(1n)).address.toString()]: "TodoChild(1)",
28+
[(await TodoChild.fromInit(2n)).address.toString()]: "TodoChild(2)",
29+
[(await TodoChild.fromInit(3n)).address.toString()]: "TodoChild(3)",
30+
};
31+
return [[contract], addresses, [await contract.send(deployer.getSender(), { value: toNano(1) }, { $$type: "Deploy", queryId: 0n })]];
32+
},
33+
messages: {
34+
"greet 3": async () => {
35+
return [await contract.send(sender, { value: toNano(1) }, "greet 3")];
36+
},
37+
},
38+
getters: {},
39+
prev: getExample(import.meta.url).prev,
40+
next: getExample(import.meta.url).next,
41+
};
42+
</script>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Communicating with Children
2+
3+
In a parent-child relationship, the user would normally just deploy the parent. This is what's happening here when you press the <span class="mdButton blue">Deploy</span> button.
4+
5+
In this example, the user is only supposed to communicate with the parent. You can send the parent contract a message by pressing the <span class="mdButton grape">Send greet 3</span> button.
6+
7+
This message will instruct the parent to send its own `HiFromParent` message to the first 3 children. Every child will respond to the greeting by sending the parent its own `HiFromChild` back.
8+
9+
## How can parent know if a child is deployed?
10+
11+
You can't send a message to a contract until it is deployed. How can the parent guarantee that they're not communicating with a child that wasn't deployed yet?
12+
13+
The best practice is to include the *stateInit* on every message. This way, if the child isn't deployed, it will be. If the child is already deployed, this field will be ignored.
14+
15+
This is called lazy deployment.

0 commit comments

Comments
 (0)