Skip to content
Closed
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion contracts/contracts/ccip/fee_quoter/contract.tolk
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@ import "../../lib/math";
import "../router/messages"
import "../ccipsend_executor/messages"
import "../../lib/funding/withdrawable"
import "../../lib/funding/soft_freeze"
import "../../lib/versioning/upgradeable"

const CONTRACT_VERSION = "1.6.0";
const RESERVE = ton("1"); // TODO: set correct value
const RESERVE = ton("15"); // TODO: set correct value
const SOFT_FREEZE_THRESHOLD = ton("5"); // TODO: set correct value

fun onInternalMessage(in: InMessage) {
SoftFreeze{owner}.requireOperationalBalance(in.senderAddress, SOFT_FREEZE_THRESHOLD);
val msg = lazy FeeQuoter_InMessage.fromSlice(in.body);
match (msg) {
FeeQuoter_AddPriceUpdater => {
Expand Down
5 changes: 4 additions & 1 deletion contracts/contracts/ccip/offramp/contract.tolk
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import "../common/types.tolk"
import "../../deployable/types.tolk"
import "../../lib/access/ownable_2step.tolk"
import "../../lib/funding/withdrawable.tolk"
import "../../lib/funding/soft_freeze"
import "../../lib/crypto/merkle_multi_proof.tolk"
import "../../lib/ocr/multi_ocr3_base"
import "../../lib/ocr/types"
Expand All @@ -23,9 +24,11 @@ import "../../lib/receiver/types"
import "../../lib/deployable/namespace"

const CONTRACT_VERSION = "1.6.0";
const RESERVE = ton("5");
const RESERVE = ton("15"); // TODO: set correct value
const SOFT_FREEZE_THRESHOLD = ton("5"); // TODO: set correct value

fun onInternalMessage(in:InMessage) {
SoftFreeze{owner}.requireOperationalBalance(in.senderAddress, SOFT_FREEZE_THRESHOLD);
val msg = lazy OffRamp_InMessage.fromSlice(in.body);

match(msg) {
Expand Down
4 changes: 4 additions & 0 deletions contracts/contracts/ccip/onramp/contract.tolk
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import "messages"

import "../common/types";
import "../../lib/access/ownable_2step";
import "../../lib/funding/soft_freeze"
import "../../lib/utils";
import "../ccipsend_executor/storage"
import "../ccipsend_executor/types"
Expand All @@ -20,8 +21,10 @@ import "../fee_quoter/messages"
import "../fee_quoter/types"

const CONTRACT_VERSION = "1.6.0";
const SOFT_FREEZE_THRESHOLD = ton("5"); // TODO: set correct value

fun onInternalMessage(in: InMessage) {
SoftFreeze{owner}.requireOperationalBalance(in.senderAddress, SOFT_FREEZE_THRESHOLD);
val msg = lazy OnRamp_InMessage.fromSlice(in.body);
match (msg) {
// Permissionless
Expand Down Expand Up @@ -364,6 +367,7 @@ Only the owner is authorized to call this function.
*/
fun onSetDynamicConfig(mutate st: OnRamp_Storage, msg: OnRamp_SetDynamicConfig) {
// feeQuoter and feeAggregator are typed as address, so they're guaranteed not null (vs `address?`)
assert (msg.config.reserve > SOFT_FREEZE_THRESHOLD, Error.InvalidReserve);
st.config = msg.config.toCell();

emit(
Expand Down
1 change: 1 addition & 0 deletions contracts/contracts/ccip/onramp/errors.tolk
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ enum Error {
InvalidConfig
UnknownToken
InsufficientValue
InvalidReserve
}
5 changes: 4 additions & 1 deletion contracts/contracts/ccip/router/contract.tolk
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import "errors"
import "../onramp/messages"
import "../onramp/types"
import "../../lib/funding/withdrawable.tolk"
import "../../lib/funding/soft_freeze.tolk"
import "../../deployable/types"
import "../../lib/access/ownable_2step"
import "../../lib/utils"
Expand All @@ -17,9 +18,11 @@ import "../offramp/types"
import "events"

const CONTRACT_VERSION = "1.6.0";
const RESERVE = ton("1"); // TODO: set correct value
const RESERVE = ton("15"); // TODO: set correct value
const SOFT_FREEZE_THRESHOLD = ton("5"); // TODO: set correct value

fun onInternalMessage(in: InMessage) {
SoftFreeze{owner}.requireOperationalBalance(in.senderAddress, SOFT_FREEZE_THRESHOLD);
val msg = lazy Router_InMsg.fromSlice(in.body);
match (msg) {
// Sender must be owner
Expand Down
29 changes: 29 additions & 0 deletions contracts/contracts/lib/funding/soft_freeze.tolk
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// -- Freeze protection against value leakage --
// This package serves to prevent contracts with leaky message handlers from
// draining. It ensures balance consumption remains limited to rent.


// getFacilityId(stringCrc32(com.chainlink.ton.lib.funding.SoftFreeze))
const SoftFreeze_FACILITY_ID = 70;
const SoftFreeze_ERROR_CODE = SoftFreeze_FACILITY_ID * 100;

enum SoftFreeze_Error {
BelowOperationalBalance = SoftFreeze_ERROR_CODE; // only the owner may call this function when soft frozen.
}


// A soft freeze mechanism that restricts permissionless operations
// when the contract's balance falls below a defined threshold.
// Only the owner can perform operations when soft frozen.
struct SoftFreeze {
// Allowed to bypass freeze checks
owner: () -> address,
}

// Throws if balance is below threshold and sender is not owner.
@inline
fun SoftFreeze.requireOperationalBalance(self, sender: address, threshold: coins) {
if (contract.getOriginalBalance() < threshold) {
assert (sender == self.owner()) throw (SoftFreeze_Error.BelowOperationalBalance as int);
};
}
68 changes: 64 additions & 4 deletions contracts/tests/ccip/OffRamp.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import * as coverage from '../coverage/coverage'
import { errorCode, facilityId } from '../../wrappers/utils'

import { newWithdrawableSpec } from '../lib/funding/WithdrawableSpec'
import { newSoftFreezeSpec } from '../lib/funding/SoftFreezeSpec'
import * as UpgradeableSpec from '../lib/versioning/UpgradeableSpec'
import * as TypeAndVersionSpec from '../lib/versioning/TypeAndVersionSpec'
import * as ownable2StepSpec from '../../tests/lib/access/Ownable2StepSpec'
Expand Down Expand Up @@ -138,7 +139,7 @@ async function deployOffRampContract(

const contract = blockchain.openContract(of.OffRamp.createFromConfig(data, code))
const deployer = await blockchain.treasury('deployer')
await contract.sendDeploy(deployer.getSender(), toNano('0.05'))
await contract.sendDeploy(deployer.getSender(), of.SOFT_FREEZE_THRESHOLD)
return contract
}

Expand Down Expand Up @@ -171,6 +172,61 @@ describe('OffRamp - Withdrawable Tests', () => {
])
})

describe('OffRamp - SoftFreeze Tests', () => {
const softFreezeSpec = newSoftFreezeSpec({
getCode: () => compile('OffRamp'),
ContractConstructor: of.OffRamp,
softFreezeThreshold: of.SOFT_FREEZE_THRESHOLD,
deployContract: async (blockchain, owner, initialBalance) => {
const offRamp = await deployOffRampContract(blockchain, owner)
// Adjust balance to match initial balance requirement
const currentBalance = (await blockchain.getContract(offRamp.address)).balance
const result = await offRamp.sendWithdraw(owner.getSender(), initialBalance, {
queryId: 0n,
amount: 0n,
destination: owner.address,
reserve: initialBalance,
drainAllAvailable: true,
})
expect(result.transactions).toHaveTransaction({
from: offRamp.address,
to: owner.address,
success: true,
value(x) {
if (!x) return false
return x >= currentBalance - initialBalance - toNano('0.05') // account for gas
},
})
expect((await blockchain.getContract(offRamp.address)).balance).toBe(initialBalance)
return offRamp
},
callOwnerMethod: async (contract, sender) => {
return contract.sendUpdateSourceChainConfigs(sender.getSender(), {
value: toNano('0.1'),
configs: [],
})
},
callNonOwnerMethod: async (contract, sender) => {
return contract.sendManualExecute(sender.getSender(), {
value: toNano('0.1'),
report: {
sourceChainSelector: 0n,
messages: [],
offchainTokenData: [],
proofs: [],
proofFlagBits: 0n,
},
})
},
})
softFreezeSpec.run([
{
code: 'OffRamp',
name: 'offramp',
},
])
})

// TODO when we have a new version
// describe('OffRamp - Upgrade Tests', () => {
// const upgradeSpec = UpgradeableSpec.newUpgradeSpec(
Expand All @@ -192,7 +248,7 @@ describe('OffRamp - Withdrawable Tests', () => {
// ),
// )
// const deployer = await blockchain.treasury('deployer')
// await contract.sendDeploy(deployer.getSender(), toNano('0.05'))
// await contract.sendDeploy(deployer.getSender(), rt.SOFT_FREEZE_THRESHOLD)
// return contract
// },
// )
Expand Down Expand Up @@ -626,7 +682,7 @@ describe('OffRamp - Unit Tests', () => {

offRamp = blockchain.openContract(of.OffRamp.createFromConfig(data, code))

let result = await offRamp.sendDeploy(deployer.getSender(), toNano('0.05'))
let result = await offRamp.sendDeploy(deployer.getSender(), of.SOFT_FREEZE_THRESHOLD * 10n)
expect(result.transactions).toHaveTransaction({
from: deployer.address,
to: offRamp.address,
Expand Down Expand Up @@ -666,7 +722,11 @@ describe('OffRamp - Unit Tests', () => {

router = blockchain.openContract(rt.Router.createFromConfig(data, code))

const result = await router.sendInternal(deployer.getSender(), toNano('1'), Cell.EMPTY)
const result = await router.sendInternal(
deployer.getSender(),
rt.SOFT_FREEZE_THRESHOLD * 2n,
Cell.EMPTY,
)

expect(result.transactions).toHaveTransaction({
from: deployer.address,
Expand Down
68 changes: 67 additions & 1 deletion contracts/tests/ccip/feequoter/FeeQuoter.spec.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { compile } from '@ton/blueprint'
import { Blockchain, SandboxContract, TreasuryContract } from '@ton/sandbox'
import { beginCell, Cell, SendMode, Slice, toNano } from '@ton/core'
import { crc32 } from 'zlib'

import * as coverage from '../../coverage/coverage'
import { errorCode, facilityId } from '../../../wrappers/utils'

import { setupTestFeeQuoter } from '../helpers/SetUp'
import { newWithdrawableSpec } from '../../lib/funding/WithdrawableSpec'
import { newSoftFreezeSpec } from '../../lib/funding/SoftFreezeSpec'
import * as TypeAndVersionSpec from '../../lib/versioning/TypeAndVersionSpec'
import * as UpgradeableSpec from '../../lib/versioning/UpgradeableSpec'
import * as ownable2StepSpec from '../../../tests/lib/access/Ownable2StepSpec'

import * as ownable2step from '../../../wrappers/libraries/access/Ownable2Step'
import * as fq from '../../../wrappers/ccip/FeeQuoter'
import { EVM_ADDRESS } from '../router/Router.Setup'

describe('FeeQuoter - Withdrawable Tests', () => {
const withdrawableSpec = newWithdrawableSpec({
Expand All @@ -29,6 +32,68 @@ describe('FeeQuoter - Withdrawable Tests', () => {
])
})

describe('FeeQuoter - SoftFreeze Tests', () => {
const softFreezeSpec = newSoftFreezeSpec({
getCode: () => compile('FeeQuoter'),
ContractConstructor: fq.FeeQuoter,
softFreezeThreshold: fq.SOFT_FREEZE_THRESHOLD,
deployContract: async (blockchain, owner, initialBalance) => {
const feeQuoter = await setupTestFeeQuoter(owner, blockchain)
// Adjust balance to match initial balance requirement
const currentBalance = (await blockchain.getContract(feeQuoter.address)).balance
const result = await feeQuoter.sendWithdraw(owner.getSender(), initialBalance, {
queryId: 0n,
amount: 0n,
destination: owner.address,
reserve: initialBalance,
drainAllAvailable: true,
})
expect(result.transactions).toHaveTransaction({
from: feeQuoter.address,
to: owner.address,
success: true,
value(x) {
if (!x) return false
return x >= currentBalance - initialBalance - toNano('0.05') // account for gas
},
})
expect((await blockchain.getContract(feeQuoter.address)).balance).toBe(initialBalance)
return feeQuoter
},
callOwnerMethod: async (contract, sender) => {
return contract.sendUpdateFeeTokens(sender.getSender(), {
value: toNano('0.1'),
msg: {
add: new Map(),
remove: [],
},
})
},
callNonOwnerMethod: async (contract, sender) => {
return contract.sendGetValidatedFee(sender.getSender(), {
value: toNano('0.1'),
msg: {
msg: {
receiver: EVM_ADDRESS,
data: Cell.EMPTY,
tokenAmounts: [],
feeToken: null,
destChainSelector: 909606746561742123n, // CHAINSEL_EVM_TEST_90000001
extraArgs: new Cell(),
},
context: Cell.EMPTY.asSlice(),
},
})
},
})
softFreezeSpec.run([
{
code: 'FeeQuoter',
name: 'feequoter',
},
])
})

describe('FeeQuoter - TypeAndVersion Tests', () => {
const currentVersionSpec = TypeAndVersionSpec.newInstance({
type: fq.FeeQuoter.type(),
Expand Down Expand Up @@ -66,7 +131,7 @@ describe('FeeQuoter - TypeAndVersion Tests', () => {
// ),
// )
// const deployer = await blockchain.treasury('deployer')
// await contract.sendDeploy(deployer.getSender(), toNano('0.05'))
// await contract.sendDeploy(deployer.getSender(), fq.SOFT_FREEZE_THRESHOLD)
// return contract
// },
// )
Expand All @@ -76,6 +141,7 @@ describe('FeeQuoter - TypeAndVersion Tests', () => {
describe('FeeQuoter - Ownable Tests', () => {
it('supports ownable messages', async () => {
const blockchain = await Blockchain.create()
blockchain.now = 1
if (process.env['COVERAGE'] === 'true') {
blockchain.enableCoverage()
blockchain.verbosity.print = false
Expand Down
2 changes: 1 addition & 1 deletion contracts/tests/ccip/feequoter/FeeQuoterSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ export class FeeQuoterSetup {
async deployFeeQuoterContract(): Promise<void> {
const deployResult = await this.bind.feeQuoter.sendDeploy(
this.acc.deployer.getSender(),
toNano('1'),
feeQuoter.SOFT_FREEZE_THRESHOLD * 2n,
)

expect(deployResult.transactions).toHaveTransaction({
Expand Down
19 changes: 7 additions & 12 deletions contracts/tests/ccip/helpers/SetUp.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import { Blockchain, SandboxContract, TreasuryContract } from '@ton/sandbox'
import { generateRandomContractId, LINK_TOKEN, WRAPPED_NATIVE } from '../../../src/utils'
import {
createTimestampedPriceValue,
FeeQuoter,
FeeQuoterStorage,
TimestampedPrice,
} from '../../../wrappers/ccip/FeeQuoter'
import * as fq from '../../../wrappers/ccip/FeeQuoter'
import { compile } from '@ton/blueprint'
import { Dictionary, toNano } from '@ton/core'

Expand All @@ -15,9 +10,9 @@ export const setupTestFeeQuoter = async (
deployer: SandboxContract<TreasuryContract>,
blockchain: Blockchain,
) => {
let code = await compile('FeeQuoter')
let code = await fq.FeeQuoter.code()

let data: FeeQuoterStorage = {
let data: fq.FeeQuoterStorage = {
id: generateRandomContractId(),
ownable: {
owner: deployer.address,
Expand All @@ -27,7 +22,7 @@ export const setupTestFeeQuoter = async (
maxFeeJuelsPerMsg: 1000000n,
linkToken: LINK_TOKEN,
tokenPriceStalenessThreshold: 1000n,
usdPerToken: Dictionary.empty(Dictionary.Keys.Address(), createTimestampedPriceValue()),
usdPerToken: Dictionary.empty(Dictionary.Keys.Address(), fq.createTimestampedPriceValue()),
premiumMultiplierWeiPerEth: Dictionary.empty(
Dictionary.Keys.Address(),
Dictionary.Values.BigUint(64),
Expand All @@ -38,10 +33,10 @@ export const setupTestFeeQuoter = async (
data.usdPerToken.set(WRAPPED_NATIVE, {
value: 123n,
timestamp: BigInt(Math.floor(Date.now() / 1000)), // Convert milliseconds to seconds for uint32
} as TimestampedPrice)
let feeQuoter = blockchain.openContract(FeeQuoter.createFromConfig(data, code))
} as fq.TimestampedPrice)
let feeQuoter = blockchain.openContract(fq.FeeQuoter.createFromConfig(data, code))

let result = await feeQuoter.sendDeploy(deployer.getSender(), toNano('0.05'))
let result = await feeQuoter.sendDeploy(deployer.getSender(), fq.SOFT_FREEZE_THRESHOLD)
expect(result.transactions).toHaveTransaction({
from: deployer.address,
to: feeQuoter.address,
Expand Down
Loading
Loading