Skip to content

Commit bdff7c6

Browse files
authored
Add CallProxy_TopUp msg (#116)
* Add CallProxy_TopUp msg * Add CallProxy top-up static limit
1 parent 0341f90 commit bdff7c6

File tree

6 files changed

+184
-47
lines changed

6 files changed

+184
-47
lines changed

contracts/contracts/mcms/call_proxy.tolk

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
tolk 1.0
22

3+
// --- Messages - incoming ---
4+
5+
/// @dev Top up contract with TON coins.
6+
/// Contract might receive/hold TON as part of the maintenance process.
7+
///
8+
struct (0x3b3d63b8) CallProxy_TopUp {
9+
/// Query ID of the change owner request.
10+
queryId: uint64;
11+
}
12+
13+
/// @dev Union of all incoming messages.
14+
type CallProxy_InMessage = CallProxy_TopUp
15+
316
// --- Storage ---
417

518
/// CallProxy contract storage, auto-serialized to/from cell.
@@ -17,21 +30,50 @@ fun CallProxy_Data.fromContractData() {
1730
return CallProxy_Data.fromCell(contract.getData());
1831
}
1932

33+
// --- Constants ---
34+
35+
const RENT_MAX_TON: int = ton("0.1");
36+
37+
const ERROR_CONTRACT_MAX_FUNDED = 101;
38+
const ERROR_VALUE_OUT_OF_BOUNDS = 102;
39+
2040
// --- Message handlers ---
2141

2242
/// @notice a contract which acts as a forwarder that forwards the input from
2343
/// any caller to a target contract.
2444
fun onInternalMessage(in: InMessage) {
25-
/// Load the contract storage as target address and proxy the message
26-
val target = CallProxy_Data.fromContractData().target;
27-
28-
// Proxy a message to the target
29-
createMessage({
30-
bounce: false,
31-
value: 0,
32-
dest: target,
33-
body: in.body
34-
}).send(SEND_MODE_REGULAR | SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE);
45+
val msg = lazy CallProxy_InMessage.fromSlice(in.body);
46+
match (msg) {
47+
CallProxy_TopUp => {
48+
// Top up the contract balance
49+
// Notice: we put a simple static limit on max rent this contract can hold
50+
// - funds are only needed for rent
51+
// - funds can't be withdrawn once deposited
52+
53+
val current = contract.getOriginalBalance() - in.valueCoins;
54+
if (current > RENT_MAX_TON) {
55+
throw(ERROR_CONTRACT_MAX_FUNDED)
56+
}
57+
58+
if (in.valueCoins > RENT_MAX_TON) {
59+
throw(ERROR_VALUE_OUT_OF_BOUNDS)
60+
}
61+
62+
return;
63+
}
64+
else => {
65+
/// Load the contract storage as target address and proxy the message
66+
val data = lazy CallProxy_Data.fromCell(contract.getData());
67+
68+
// Proxy a message to the target
69+
createMessage({
70+
bounce: false,
71+
value: 0,
72+
dest: data.target,
73+
body: in.body
74+
}).send(SEND_MODE_REGULAR | SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE);
75+
}
76+
}
3577
}
3678

3779
// --- Getters ---

contracts/contracts/mcms/mcms.tolk

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -464,7 +464,7 @@ struct Op {
464464
nonce: uint40;
465465
to: address;
466466
value: coins;
467-
data: cell; // body of the operation
467+
data: cell; // body of the operation
468468
}
469469

470470
// --- Methods ---
@@ -504,7 +504,7 @@ fun MCMS<T>.setRoot(
504504
validUntil: uint32,
505505
metadata: RootMetadata,
506506
metadataProof: Iterator<uint256>,
507-
signatures: cell, // vec<Signature>
507+
signatures: cell // vec<Signature>
508508
) {
509509
/// Construct the message that should be signed by the signers.
510510
val signedHash = beginCell()

contracts/tests/mcms/CallProxy.spec.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ describe('CallProxy', () => {
3939
const deployResult = await bind.callProxy.sendInternal(
4040
deployer.getSender(),
4141
toNano('0.05'),
42-
beginCell().endCell(), // Empty body for deployment
42+
callProxy.builder.message.in.topUp.encode({ queryId: 1n }), // TopUp message to deploy
4343
)
4444

4545
expect(deployResult.transactions).toHaveTransaction({
@@ -50,6 +50,11 @@ describe('CallProxy', () => {
5050
})
5151
})
5252

53+
it('Should compute crc32 opcodes', async () => {
54+
// In opcodes
55+
expect(callProxy.opcodes.in.TopUp).toBe(0x3b3d63b8)
56+
})
57+
5358
it('should deploy and set target correctly', async () => {
5459
// Verify the contract deployed successfully
5560
expect(bind.callProxy.address).toBeDefined()
@@ -63,6 +68,56 @@ describe('CallProxy', () => {
6368
expect(id).toBe(MOCK_TARGET_ID)
6469
})
6570

71+
it('should limit excess top-up', async () => {
72+
const r = await bind.callProxy.sendInternal(
73+
deployer.getSender(),
74+
toNano('100.05'),
75+
callProxy.builder.message.in.topUp.encode({ queryId: 1n }),
76+
)
77+
78+
expect(r.transactions).toHaveTransaction({
79+
from: deployer.address,
80+
to: bind.callProxy.address,
81+
exitCode: callProxy.Errors.ValueOutOfBounds,
82+
})
83+
84+
const r1 = await bind.callProxy.sendInternal(
85+
deployer.getSender(),
86+
toNano('0.05'),
87+
callProxy.builder.message.in.topUp.encode({ queryId: 1n }),
88+
)
89+
90+
expect(r1.transactions).toHaveTransaction({
91+
from: deployer.address,
92+
to: bind.callProxy.address,
93+
success: true,
94+
})
95+
96+
const r2 = await bind.callProxy.sendInternal(
97+
deployer.getSender(),
98+
toNano('0.08'),
99+
callProxy.builder.message.in.topUp.encode({ queryId: 1n }),
100+
)
101+
102+
expect(r2.transactions).toHaveTransaction({
103+
from: deployer.address,
104+
to: bind.callProxy.address,
105+
success: true,
106+
})
107+
108+
const r3 = await bind.callProxy.sendInternal(
109+
deployer.getSender(),
110+
toNano('0.08'),
111+
callProxy.builder.message.in.topUp.encode({ queryId: 1n }),
112+
)
113+
114+
expect(r3.transactions).toHaveTransaction({
115+
from: deployer.address,
116+
to: bind.callProxy.address,
117+
exitCode: callProxy.Errors.ContractMaxFunded,
118+
})
119+
})
120+
66121
it('should forward messages to target', async () => {
67122
// Create a test message body
68123
const testBody = beginCell()

contracts/tests/mcms/Integration.spec.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ import { merkleProof } from '../../src/mcms'
2121
describe('MCMS - IntegrationTest', () => {
2222
let blockchain: Blockchain
2323

24+
// TODO: blockchain global chain ID (will need to be signed int)
25+
let chainId = -239n
26+
2427
var code: {
2528
mcms: Cell
2629
timelock: Cell
@@ -530,15 +533,15 @@ describe('MCMS - IntegrationTest', () => {
530533
}))
531534
const validUntil = BigInt(blockchain.now || 0) + 2n * 60n * 60n // block.timestamp + 2 hours
532535
const metadata = {
533-
chainId: -239n, // TODO: blockchain global chain ID (will need to be signed int)
536+
chainId,
534537
multiSig: bind.mcmsPropose.address,
535538
preOpCount: 0n,
536539
postOpCount: 1n,
537540
overridePreviousRoot: false,
538541
}
539542
const ops: mcms.Op[] = [
540543
{
541-
chainId: -239n, // TODO: blockchain global chain ID (will need to be signed int)
544+
chainId,
542545
multiSig: bind.mcmsPropose.address,
543546
nonce: 0n,
544547
to: bind.timelock.address,
@@ -648,15 +651,15 @@ describe('MCMS - IntegrationTest', () => {
648651
}))
649652
const validUntil = BigInt(blockchain.now || 0) + 2n * 60n * 60n // block.timestamp + 2 hours
650653
const metadata = {
651-
chainId: -239n, // TODO: blockchain global chain ID (will need to be signed int)
654+
chainId,
652655
multiSig: bind.mcmsPropose.address,
653656
preOpCount: 1n,
654657
postOpCount: 2n,
655658
overridePreviousRoot: false,
656659
}
657660
const ops: mcms.Op[] = [
658661
{
659-
chainId: -239n, // TODO: blockchain global chain ID (will need to be signed int)
662+
chainId,
660663
multiSig: bind.mcmsPropose.address,
661664
nonce: 1n,
662665
to: bind.timelock.address,
@@ -779,15 +782,15 @@ describe('MCMS - IntegrationTest', () => {
779782
}))
780783
const validUntil = BigInt(blockchain.now || 0) + 2n * 60n * 60n // block.timestamp + 2 hours
781784
const metadata = {
782-
chainId: -239n, // TODO: blockchain global chain ID (will need to be signed int)
785+
chainId,
783786
multiSig: bind.mcmsBypass.address,
784787
preOpCount: 0n,
785788
postOpCount: 1n,
786789
overridePreviousRoot: false,
787790
}
788791
const ops: mcms.Op[] = [
789792
{
790-
chainId: -239n, // TODO: blockchain global chain ID (will need to be signed int)
793+
chainId,
791794
multiSig: bind.mcmsBypass.address,
792795
nonce: 0n,
793796
to: bind.timelock.address,
@@ -892,15 +895,15 @@ describe('MCMS - IntegrationTest', () => {
892895
}))
893896
const validUntil = BigInt(blockchain.now || 0) + 2n * 60n * 60n // block.timestamp + 2 hours
894897
const metadata = {
895-
chainId: -239n, // TODO: blockchain global chain ID (will need to be signed int)
898+
chainId,
896899
multiSig: bind.mcmsPropose.address,
897900
preOpCount: 2n,
898901
postOpCount: 3n,
899902
overridePreviousRoot: false,
900903
}
901904
const ops: mcms.Op[] = [
902905
{
903-
chainId: -239n, // TODO: blockchain global chain ID (will need to be signed int)
906+
chainId,
904907
multiSig: bind.mcmsPropose.address,
905908
nonce: 2n,
906909
to: bind.timelock.address,
@@ -972,15 +975,15 @@ describe('MCMS - IntegrationTest', () => {
972975
}))
973976
const validUntil = BigInt(blockchain.now || 0) + 2n * 60n * 60n // block.timestamp + 2 hours
974977
const metadata = {
975-
chainId: -239n, // TODO: blockchain global chain ID (will need to be signed int)
978+
chainId,
976979
multiSig: bind.mcmsVeto.address,
977980
preOpCount: 0n,
978981
postOpCount: 1n,
979982
overridePreviousRoot: false,
980983
}
981984
const ops: mcms.Op[] = [
982985
{
983-
chainId: -239n, // TODO: blockchain global chain ID (will need to be signed int)
986+
chainId,
984987
multiSig: bind.mcmsVeto.address,
985988
nonce: 0n,
986989
to: bind.timelock.address,
@@ -1097,15 +1100,15 @@ describe('MCMS - IntegrationTest', () => {
10971100
}))
10981101
const validUntil = BigInt(blockchain.now || 0) + 2n * 60n * 60n // block.timestamp + 2 hours
10991102
const metadata = {
1100-
chainId: -239n, // TODO: blockchain global chain ID (will need to be signed int)
1103+
chainId,
11011104
multiSig: bind.mcmsPropose.address,
11021105
preOpCount: 3n,
11031106
postOpCount: 4n,
11041107
overridePreviousRoot: false,
11051108
}
11061109
const ops: mcms.Op[] = [
11071110
{
1108-
chainId: -239n, // TODO: blockchain global chain ID (will need to be signed int)
1111+
chainId,
11091112
multiSig: bind.mcmsPropose.address,
11101113
nonce: 3n,
11111114
to: bind.timelock.address,

contracts/wrappers/mcms/CallProxy.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,15 @@ import {
88
Sender,
99
SendMode,
1010
} from '@ton/core'
11+
import { crc32 } from 'zlib'
1112
import { CellCodec } from '../utils'
1213

14+
// @dev Top up contract with TON coins.
15+
export type TopUp = {
16+
// Query ID of the change owner request.
17+
queryId: bigint
18+
}
19+
1320
// CallProxy contract storage
1421
export type ContractData = {
1522
/// ID allows multiple independent instances, since contract address depends on initial state.
@@ -19,7 +26,39 @@ export type ContractData = {
1926
target: Address
2027
}
2128

29+
export const opcodes = {
30+
in: {
31+
TopUp: crc32('CallProxy_TopUp'),
32+
},
33+
out: {},
34+
}
35+
36+
export enum Errors {
37+
ContractMaxFunded = 101,
38+
ValueOutOfBounds = 102,
39+
}
40+
2241
export const builder = {
42+
message: {
43+
in: {
44+
// Creates a new `CallProxy_TopUp` message.
45+
topUp: {
46+
encode: (msg: TopUp): Cell => {
47+
return beginCell() // break line
48+
.storeUint(opcodes.in.TopUp, 32)
49+
.storeUint(msg.queryId, 64)
50+
.endCell()
51+
},
52+
decode: (cell: Cell): TopUp => {
53+
const s = cell.beginParse()
54+
s.skip(32) // skip opcode
55+
return {
56+
queryId: s.loadUintBig(64),
57+
}
58+
},
59+
},
60+
},
61+
},
2362
data: (() => {
2463
// Creates a new `CallProxy_Data` contract data cell
2564
const contractData: CellCodec<ContractData> = {

0 commit comments

Comments
 (0)